panopticon-cli 0.6.5 → 0.6.6
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/README.md +2 -2
- package/dist/{agents-DfYify9s.js → agents-CfFDs52G.js} +14 -14
- package/dist/{agents-DfYify9s.js.map → agents-CfFDs52G.js.map} +1 -1
- package/dist/{agents-BKsVoIc9.js → agents-D_2oRFVf.js} +1 -1
- package/dist/{archive-planning-BJrZ3tmN.js → archive-planning-D97ziGec.js} +3 -3
- package/dist/{archive-planning-BJrZ3tmN.js.map → archive-planning-D97ziGec.js.map} +1 -1
- package/dist/{archive-planning-C3m3hfa5.js → archive-planning-DK90wn9Q.js} +1 -1
- package/dist/{browser-Cvdznzc0.js → browser-CX7jXfXX.js} +1 -1
- package/dist/{browser-Cvdznzc0.js.map → browser-CX7jXfXX.js.map} +1 -1
- package/dist/{clean-planning-DvhZAUv4.js → clean-planning-D_lz4aQq.js} +2 -2
- package/dist/{clean-planning-DvhZAUv4.js.map → clean-planning-D_lz4aQq.js.map} +1 -1
- package/dist/clean-planning-x1S-JdmO.js +2 -0
- package/dist/cli/index.js +291 -760
- package/dist/cli/index.js.map +1 -1
- package/dist/{close-issue-Dr7yZmrr.js → close-issue-CaFE0stN.js} +11 -7
- package/dist/close-issue-CaFE0stN.js.map +1 -0
- package/dist/close-issue-CjcfZI9s.js +2 -0
- package/dist/compact-beads-B0_qE1w3.js +2 -0
- package/dist/{compact-beads-BCOtIIRl.js → compact-beads-CjFkteSU.js} +2 -2
- package/dist/{compact-beads-BCOtIIRl.js.map → compact-beads-CjFkteSU.js.map} +1 -1
- package/dist/{config-CRzMQRgA.js → config-BQNKsi9G.js} +2 -2
- package/dist/{config-CRzMQRgA.js.map → config-BQNKsi9G.js.map} +1 -1
- package/dist/{config-BYgUzQ21.js → config-agyKgF5C.js} +1 -1
- package/dist/{config-yaml-BgOACZAB.js → config-yaml-DGbLSMCa.js} +1 -1
- package/dist/{config-yaml-BgOACZAB.js.map → config-yaml-DGbLSMCa.js.map} +1 -1
- package/dist/{config-yaml-fdyvyL0S.js → config-yaml-Dqt4FWQH.js} +1 -1
- package/dist/dashboard/{acceptance-criteria-e5iiHlRx.js → acceptance-criteria-Dk9hhiYj.js} +1 -1
- package/dist/dashboard/{acceptance-criteria-e5iiHlRx.js.map → acceptance-criteria-Dk9hhiYj.js.map} +1 -1
- package/dist/dashboard/{agent-enrichment-C67LJBgD.js → agent-enrichment-DdO7ZqjI.js} +11 -7
- package/dist/dashboard/agent-enrichment-DdO7ZqjI.js.map +1 -0
- package/dist/dashboard/{agent-enrichment-Cq0P1cNZ.js → agent-enrichment-dLeGE1fX.js} +1 -1
- package/dist/dashboard/{agents-YyO6t5Xa.js → agents-DCpQQ_W5.js} +14 -14
- package/dist/dashboard/{agents-YyO6t5Xa.js.map → agents-DCpQQ_W5.js.map} +1 -1
- package/dist/dashboard/{agents-BVBVCyat.js → agents-Dgh2TjSp.js} +1 -1
- package/dist/dashboard/{archive-planning-h-hAjk0P.js → archive-planning-BmW9UDTr.js} +3 -3
- package/dist/dashboard/{archive-planning-h-hAjk0P.js.map → archive-planning-BmW9UDTr.js.map} +1 -1
- package/dist/dashboard/{archive-planning-CScs1MOC.js → archive-planning-C3Ebf9yC.js} +1 -1
- package/dist/dashboard/{beads-qNB0yAHV.js → beads-Bv-AdX7G.js} +3 -3
- package/dist/dashboard/{beads-qNB0yAHV.js.map → beads-Bv-AdX7G.js.map} +1 -1
- package/dist/dashboard/{beads-D_FRedEJ.js → beads-By6-X07V.js} +1 -1
- package/dist/dashboard/clean-planning-D60L8rPY.js +2 -0
- package/dist/dashboard/{clean-planning-qafw99vY.js → clean-planning-VEJu5suh.js} +2 -2
- package/dist/dashboard/{clean-planning-qafw99vY.js.map → clean-planning-VEJu5suh.js.map} +1 -1
- package/dist/dashboard/close-issue-C2KeSKKJ.js +2 -0
- package/dist/dashboard/{close-issue-DfIggeZD.js → close-issue-DtKdsSTm.js} +11 -7
- package/dist/dashboard/close-issue-DtKdsSTm.js.map +1 -0
- package/dist/dashboard/compact-beads-C7BN5N11.js +2 -0
- package/dist/dashboard/{compact-beads-Dt0qTqsC.js → compact-beads-D8Vt3qyv.js} +2 -2
- package/dist/dashboard/{compact-beads-Dt0qTqsC.js.map → compact-beads-D8Vt3qyv.js.map} +1 -1
- package/dist/dashboard/{config-CUREjHP7.js → config-CDkGjnwy.js} +2 -2
- package/dist/dashboard/{config-CUREjHP7.js.map → config-CDkGjnwy.js.map} +1 -1
- package/dist/dashboard/{config-BeI3uy-8.js → config-CTXkBATQ.js} +1 -1
- package/dist/dashboard/{database-CozA13Wy.js → database-DhqASALP.js} +1 -1
- package/dist/dashboard/{database-C0y0hXBx.js → database-cxmQryoh.js} +2 -2
- package/dist/dashboard/{database-C0y0hXBx.js.map → database-cxmQryoh.js.map} +1 -1
- package/dist/dashboard/{dist-src-oG2iHzgI.js → dist-src-DTm11oQr.js} +1 -1
- package/dist/dashboard/{dist-src-oG2iHzgI.js.map → dist-src-DTm11oQr.js.map} +1 -1
- package/dist/dashboard/{event-store-D7kLBd07.js → event-store-VWWUmOfn.js} +1 -1
- package/dist/dashboard/{event-store-O9q0Gweh.js → event-store-vSmAA3Zp.js} +9 -4
- package/dist/dashboard/event-store-vSmAA3Zp.js.map +1 -0
- package/dist/dashboard/{factory-BnLdiQW-.js → factory-C8nhLGHB.js} +3 -3
- package/dist/dashboard/{factory-BnLdiQW-.js.map → factory-C8nhLGHB.js.map} +1 -1
- package/dist/dashboard/{feedback-writer-DyovUANg.js → feedback-writer-CudSe1WK.js} +2 -2
- package/dist/dashboard/{feedback-writer-DyovUANg.js.map → feedback-writer-CudSe1WK.js.map} +1 -1
- package/dist/dashboard/{feedback-writer-gSUv_W0h.js → feedback-writer-Wgv1cd1r.js} +1 -1
- package/dist/dashboard/{git-utils-BJRioREj.js → git-utils-C1m4SwAe.js} +1 -1
- package/dist/dashboard/{git-utils-BJRioREj.js.map → git-utils-C1m4SwAe.js.map} +1 -1
- package/dist/dashboard/{git-utils-BtCRddq3.js → git-utils-DQI8EYoj.js} +1 -1
- package/dist/dashboard/{github-app-XO-LBUGk.js → github-app-DClWjjHr.js} +1 -1
- package/dist/dashboard/{github-app-XO-LBUGk.js.map → github-app-DClWjjHr.js.map} +1 -1
- package/dist/dashboard/{health-events-db-584nYgJB.js → health-events-db-BMXQfInV.js} +1 -1
- package/dist/dashboard/{health-events-db-B3ChzN65.js → health-events-db-Do4NrOhC.js} +2 -2
- package/dist/dashboard/{health-events-db-B3ChzN65.js.map → health-events-db-Do4NrOhC.js.map} +1 -1
- package/dist/dashboard/{hooks-CKhs3N68.js → hooks-CB4T47NC.js} +1 -1
- package/dist/dashboard/{hooks-CErbP8Oq.js → hooks-CjqXOlNb.js} +2 -2
- package/dist/dashboard/{hooks-CErbP8Oq.js.map → hooks-CjqXOlNb.js.map} +1 -1
- package/dist/dashboard/hume-CA2pftu_.js +3 -0
- package/dist/dashboard/{hume-CX_U3Qha.js → hume-JsAlMOJC.js} +2 -2
- package/dist/dashboard/{hume-CX_U3Qha.js.map → hume-JsAlMOJC.js.map} +1 -1
- package/dist/dashboard/{inspect-agent-B57kGDUV.js → inspect-agent-7eour7EA.js} +3 -3
- package/dist/dashboard/{inspect-agent-B57kGDUV.js.map → inspect-agent-7eour7EA.js.map} +1 -1
- package/dist/dashboard/{io-yGovuG4U.js → io-CWlFW78i.js} +1 -1
- package/dist/dashboard/{io-AJg-mzFi.js → io-DKS6359z.js} +1 -1
- package/dist/dashboard/{io-AJg-mzFi.js.map → io-DKS6359z.js.map} +1 -1
- package/dist/dashboard/issue-id-vwYJdsf8.js +62 -0
- package/dist/dashboard/issue-id-vwYJdsf8.js.map +1 -0
- package/dist/dashboard/{issue-service-singleton-DQK42EqH.js → issue-service-singleton-Co__-6kL.js} +1 -1
- package/dist/dashboard/{issue-service-singleton-sb2HkB9f.js → issue-service-singleton-Wv4xBm3y.js} +7 -7
- package/dist/dashboard/{issue-service-singleton-sb2HkB9f.js.map → issue-service-singleton-Wv4xBm3y.js.map} +1 -1
- package/dist/dashboard/{label-cleanup-CZEsbtq9.js → label-cleanup-nVKTmIIW.js} +7 -4
- package/dist/dashboard/label-cleanup-nVKTmIIW.js.map +1 -0
- package/dist/dashboard/lifecycle-BcUmtkR4.js +7 -0
- package/dist/dashboard/{merge-agent-GLtMEsTu.js → merge-agent-CGN3TT0a.js} +1 -1
- package/dist/dashboard/{merge-agent-twroFuAh.js → merge-agent-yudQOPZc.js} +148 -46
- package/dist/dashboard/merge-agent-yudQOPZc.js.map +1 -0
- package/dist/dashboard/{paths-COdEvoXR.js → paths-BDyJ7BiV.js} +19 -2
- package/dist/dashboard/{paths-COdEvoXR.js.map → paths-BDyJ7BiV.js.map} +1 -1
- package/dist/dashboard/{pipeline-notifier-DM5AHG5Q.js → pipeline-notifier-CCSN-jar.js} +1 -1
- package/dist/dashboard/{pipeline-notifier-DM5AHG5Q.js.map → pipeline-notifier-CCSN-jar.js.map} +1 -1
- package/dist/dashboard/{plan-utils-BkCIhn3B.js → plan-utils-Bkcsqr_s.js} +3 -3
- package/dist/dashboard/{plan-utils-BkCIhn3B.js.map → plan-utils-Bkcsqr_s.js.map} +1 -1
- package/dist/dashboard/{prd-draft-D09Afalc.js → prd-draft-BD8oMkZ1.js} +2 -2
- package/dist/dashboard/{prd-draft-D09Afalc.js.map → prd-draft-BD8oMkZ1.js.map} +1 -1
- package/dist/dashboard/{projection-cache-DQ9zegkK.js → projection-cache-C0EL8s8h.js} +1 -1
- package/dist/dashboard/{projection-cache-DQ9zegkK.js.map → projection-cache-C0EL8s8h.js.map} +1 -1
- package/dist/dashboard/{projects-DyT3vSy-.js → projects-C5ozxjwP.js} +1 -1
- package/dist/dashboard/{projects-Cq3TWdPS.js → projects-CFVl4oHn.js} +25 -13
- package/dist/dashboard/projects-CFVl4oHn.js.map +1 -0
- package/dist/dashboard/{providers-Ck2sQd_F.js → providers-B5Y4H2Mg.js} +4 -4
- package/dist/dashboard/providers-B5Y4H2Mg.js.map +1 -0
- package/dist/dashboard/{providers-DVQnDekG.js → providers-csVZVPkE.js} +1 -1
- package/dist/dashboard/public/assets/{dist-CCJbQrSB.js → dist-BaQPC-c6.js} +1 -1
- package/dist/dashboard/public/assets/index-ByLmYGhW.js +212 -0
- package/dist/dashboard/public/assets/index-OEEbThNN.css +1 -0
- package/dist/dashboard/public/index.html +2 -2
- package/dist/dashboard/rally-6McpKKRa.js +3 -0
- package/dist/dashboard/{rally-Cwuae-4C.js → rally-YjFRxIiC.js} +2 -2
- package/dist/dashboard/{rally-Cwuae-4C.js.map → rally-YjFRxIiC.js.map} +1 -1
- package/dist/dashboard/{rally-api-DSUxm7EO.js → rally-api-C0WqCSkT.js} +1 -1
- package/dist/dashboard/{rally-api-DSUxm7EO.js.map → rally-api-C0WqCSkT.js.map} +1 -1
- package/dist/dashboard/{rally-api-CEH5KZi4.js → rally-api-DNttdCW4.js} +1 -1
- package/dist/dashboard/{remote-BHTTMpJJ.js → remote-Cigqjj3f.js} +2 -2
- package/dist/dashboard/{remote-BXo_iIku.js → remote-ObpNZ7hF.js} +2 -2
- package/dist/dashboard/{remote-BXo_iIku.js.map → remote-ObpNZ7hF.js.map} +1 -1
- package/dist/dashboard/{remote-agents-CTKVhFFY.js → remote-agents-Bf3GuM7t.js} +1 -1
- package/dist/dashboard/{remote-agents-C0_0LLNd.js → remote-agents-DFyjT1Le.js} +1 -1
- package/dist/dashboard/{remote-agents-C0_0LLNd.js.map → remote-agents-DFyjT1Le.js.map} +1 -1
- package/dist/dashboard/{review-status-CK3eBGyb.js → review-status-BtXqWBhS.js} +1 -1
- package/dist/dashboard/{review-status-CV55Tl-n.js → review-status-Bymwzh2i.js} +44 -4
- package/dist/dashboard/{review-status-CV55Tl-n.js.map → review-status-Bymwzh2i.js.map} +1 -1
- package/dist/dashboard/server.js +559 -253
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/{settings-CuHV-wcv.js → settings-BHlDG7TK.js} +2 -2
- package/dist/dashboard/settings-BHlDG7TK.js.map +1 -0
- package/dist/dashboard/settings-XWvDcj-D.js +2 -0
- package/dist/dashboard/{shadow-engineering-BUeZunaE.js → shadow-engineering-lIn1W_95.js} +1 -1
- package/dist/dashboard/{shadow-engineering-BUeZunaE.js.map → shadow-engineering-lIn1W_95.js.map} +1 -1
- package/dist/dashboard/{shadow-state-DHQ-kASN.js → shadow-state-BIexcxkv.js} +1 -1
- package/dist/dashboard/{shadow-state-DHQ-kASN.js.map → shadow-state-BIexcxkv.js.map} +1 -1
- package/dist/dashboard/{spawn-planning-session-8FFAqLdK.js → spawn-planning-session-33Jf-d5T.js} +6 -6
- package/dist/dashboard/{spawn-planning-session-8FFAqLdK.js.map → spawn-planning-session-33Jf-d5T.js.map} +1 -1
- package/dist/dashboard/{spawn-planning-session-U0Lqpjen.js → spawn-planning-session-D5hrVdWM.js} +1 -1
- package/dist/dashboard/{specialist-context-ColzlmGE.js → specialist-context-DGukHSn8.js} +6 -6
- package/dist/dashboard/{specialist-context-ColzlmGE.js.map → specialist-context-DGukHSn8.js.map} +1 -1
- package/dist/dashboard/{specialist-logs-BhmDpFIq.js → specialist-logs-CIw4qfTy.js} +1 -1
- package/dist/dashboard/{specialists-C6s3U6tX.js → specialists-B_zrayaP.js} +37 -36
- package/dist/dashboard/specialists-B_zrayaP.js.map +1 -0
- package/dist/dashboard/{specialists-Cny632-T.js → specialists-Cp-PgspS.js} +1 -1
- package/dist/dashboard/{test-agent-queue-tqI4VDsu.js → test-agent-queue-ypF_ecHo.js} +4 -4
- package/dist/dashboard/{test-agent-queue-tqI4VDsu.js.map → test-agent-queue-ypF_ecHo.js.map} +1 -1
- package/dist/dashboard/{tldr-daemon-BNFyS7W_.js → tldr-daemon-B_oLRD8z.js} +2 -2
- package/dist/dashboard/{tldr-daemon-BNFyS7W_.js.map → tldr-daemon-B_oLRD8z.js.map} +1 -1
- package/dist/dashboard/{tldr-daemon-A6JqC59u.js → tldr-daemon-Cfs0bXTi.js} +1 -1
- package/dist/dashboard/{tmux-DYGAVJfb.js → tmux-BzxdKItf.js} +1 -1
- package/dist/dashboard/{tmux-IlN1Slv-.js → tmux-LwG0tHhU.js} +2 -2
- package/dist/dashboard/{tmux-IlN1Slv-.js.map → tmux-LwG0tHhU.js.map} +1 -1
- package/dist/dashboard/{tracker-config-BzNLnmcE.js → tracker-config-BP59uH4V.js} +1 -1
- package/dist/dashboard/{tracker-config-CNM_5rEf.js → tracker-config-e7ph1QqT.js} +2 -2
- package/dist/dashboard/{tracker-config-CNM_5rEf.js.map → tracker-config-e7ph1QqT.js.map} +1 -1
- package/dist/dashboard/{tunnel-D2BkwU7k.js → tunnel-0RzzuXPf.js} +1 -1
- package/dist/dashboard/{tunnel-Dub2hiAA.js → tunnel-DldbBPWL.js} +2 -2
- package/dist/dashboard/{tunnel-Dub2hiAA.js.map → tunnel-DldbBPWL.js.map} +1 -1
- package/dist/dashboard/{types-CWA-o4UN.js → types-RKZjGE5N.js} +1 -1
- package/dist/dashboard/{types-CWA-o4UN.js.map → types-RKZjGE5N.js.map} +1 -1
- package/dist/dashboard/{vtt-parser-BAXygRf0.js → vtt-parser-99vFekRQ.js} +1 -1
- package/dist/dashboard/{vtt-parser-BAXygRf0.js.map → vtt-parser-99vFekRQ.js.map} +1 -1
- package/dist/dashboard/{work-agent-prompt-JYq_OugP.js → work-agent-prompt-fCg67nyo.js} +65 -10
- package/dist/dashboard/{work-agent-prompt-JYq_OugP.js.map → work-agent-prompt-fCg67nyo.js.map} +1 -1
- package/dist/dashboard/{work-type-router-Cxp8_ur2.js → work-type-router-CWVW2Wk_.js} +1 -1
- package/dist/dashboard/{work-type-router-Cxp8_ur2.js.map → work-type-router-CWVW2Wk_.js.map} +1 -1
- package/dist/dashboard/{work-type-router-Com2amST.js → work-type-router-Di5gCQwh.js} +1 -1
- package/dist/dashboard/{workflows-N1UTipYl.js → workflows-BSMipN07.js} +35 -17
- package/dist/dashboard/workflows-BSMipN07.js.map +1 -0
- package/dist/dashboard/workflows-DaYWQIS2.js +2 -0
- package/dist/dashboard/{workspace-config-cmp5_ipD.js → workspace-config-DVDR-Ukh.js} +1 -1
- package/dist/dashboard/workspace-config-DVDR-Ukh.js.map +1 -0
- package/dist/dashboard/{workspace-manager-CjpWPgzL.js → workspace-manager-BYfzs_t2.js} +1 -1
- package/dist/dashboard/{workspace-manager-D_y9ZmW_.js → workspace-manager-C7OfT62A.js} +44 -24
- package/dist/dashboard/workspace-manager-C7OfT62A.js.map +1 -0
- package/dist/{dns-BKzHm-2q.js → dns-D_aKQJjb.js} +1 -1
- package/dist/{dns-DZwOWvVO.js → dns-Yxq4NNS7.js} +1 -1
- package/dist/{dns-DZwOWvVO.js.map → dns-Yxq4NNS7.js.map} +1 -1
- package/dist/{factory-DFu3IT4r.js → factory-BRBGw6OB.js} +1 -1
- package/dist/{factory-DfzczxN1.js → factory-DzsOiZVc.js} +3 -3
- package/dist/{factory-DfzczxN1.js.map → factory-DzsOiZVc.js.map} +1 -1
- package/dist/{feedback-writer-CwdnOkPO.js → feedback-writer-ygXN5F9N.js} +2 -2
- package/dist/{feedback-writer-CwdnOkPO.js.map → feedback-writer-ygXN5F9N.js.map} +1 -1
- package/dist/{github-app-CHKwxOeQ.js → github-app-DykduJ0X.js} +1 -1
- package/dist/{github-app-CHKwxOeQ.js.map → github-app-DykduJ0X.js.map} +1 -1
- package/dist/hume-9nv1VmMV.js +3 -0
- package/dist/{hume-DnV-tDsh.js → hume-DoCbph2h.js} +2 -2
- package/dist/{hume-DnV-tDsh.js.map → hume-DoCbph2h.js.map} +1 -1
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/issue-id-CAcekoIw.js +62 -0
- package/dist/issue-id-CAcekoIw.js.map +1 -0
- package/dist/{label-cleanup-31ElPqqv.js → label-cleanup-C8R9Rspn.js} +7 -4
- package/dist/label-cleanup-C8R9Rspn.js.map +1 -0
- package/dist/{manifest-DL0oDbpv.js → manifest-B4ghOD-V.js} +1 -1
- package/dist/{manifest-DL0oDbpv.js.map → manifest-B4ghOD-V.js.map} +1 -1
- package/dist/{merge-agent-VQH9z9t8.js → merge-agent-DlUiUanN.js} +86 -33
- package/dist/merge-agent-DlUiUanN.js.map +1 -0
- package/dist/{paths-lMaxrYtT.js → paths-CDJ_HsbN.js} +19 -2
- package/dist/{paths-lMaxrYtT.js.map → paths-CDJ_HsbN.js.map} +1 -1
- package/dist/{pipeline-notifier-OJ-d3Y60.js → pipeline-notifier-XgDdCdvT.js} +1 -1
- package/dist/{pipeline-notifier-OJ-d3Y60.js.map → pipeline-notifier-XgDdCdvT.js.map} +1 -1
- package/dist/{projects-CvLepaxC.js → projects-Bk-5QhFQ.js} +25 -13
- package/dist/projects-Bk-5QhFQ.js.map +1 -0
- package/dist/{projects-DMWmPeIU.js → projects-DhU7rAVN.js} +1 -1
- package/dist/{providers-DcCPZ5K4.js → providers-DSU1vfQF.js} +4 -4
- package/dist/providers-DSU1vfQF.js.map +1 -0
- package/dist/rally-DdPvGa-w.js +3 -0
- package/dist/{rally-uUUZXp1h.js → rally-Dy00NElU.js} +1 -1
- package/dist/{rally-uUUZXp1h.js.map → rally-Dy00NElU.js.map} +1 -1
- package/dist/{remote-CkLBqLJc.js → remote-CYiOJg0q.js} +2 -2
- package/dist/{remote-CkLBqLJc.js.map → remote-CYiOJg0q.js.map} +1 -1
- package/dist/{remote-agents-C5Bd2fgt.js → remote-agents-CZXrUF4f.js} +1 -1
- package/dist/{remote-agents-C5Bd2fgt.js.map → remote-agents-CZXrUF4f.js.map} +1 -1
- package/dist/{remote-agents-BTzD-wMQ.js → remote-agents-ycHHVsgf.js} +1 -1
- package/dist/{remote-workspace-Dxghqiti.js → remote-workspace-CA33UuVI.js} +4 -4
- package/dist/{remote-workspace-Dxghqiti.js.map → remote-workspace-CA33UuVI.js.map} +1 -1
- package/dist/{review-status-2TdtHNcs.js → review-status-D6H2WOw8.js} +1 -1
- package/dist/{review-status-Bm1bWNEa.js → review-status-DEDvCKMP.js} +44 -4
- package/dist/{review-status-Bm1bWNEa.js.map → review-status-DEDvCKMP.js.map} +1 -1
- package/dist/{tracker-C_62ukEq.js → settings-BcWPTrua.js} +7 -199
- package/dist/settings-BcWPTrua.js.map +1 -0
- package/dist/shadow-state-BZzxfEGw.js +2 -0
- package/dist/{shadow-state-CFFHf05M.js → shadow-state-CE3dQfll.js} +1 -1
- package/dist/{shadow-state-CFFHf05M.js.map → shadow-state-CE3dQfll.js.map} +1 -1
- package/dist/{specialist-context-BdNFsfMG.js → specialist-context-BAUWL1Fl.js} +6 -6
- package/dist/{specialist-context-BdNFsfMG.js.map → specialist-context-BAUWL1Fl.js.map} +1 -1
- package/dist/{specialist-logs-CLztE_bE.js → specialist-logs-DQKKQV9B.js} +1 -1
- package/dist/{specialists-aUoUVWsN.js → specialists-Bfb9ATzw.js} +1 -1
- package/dist/{specialists-DEKqgkxp.js → specialists-D7Kj5o6s.js} +35 -34
- package/dist/specialists-D7Kj5o6s.js.map +1 -0
- package/dist/sync-DMfgd389.js +693 -0
- package/dist/sync-DMfgd389.js.map +1 -0
- package/dist/sync-TL6y-8K6.js +2 -0
- package/dist/{tldr-daemon-BCEFPItr.js → tldr-daemon-CFx4LXAl.js} +2 -2
- package/dist/{tldr-daemon-BCEFPItr.js.map → tldr-daemon-CFx4LXAl.js.map} +1 -1
- package/dist/{tldr-daemon-xBAx4cBE.js → tldr-daemon-D_EooADG.js} +1 -1
- package/dist/{tmux-DN6H886Y.js → tmux-CBtui_Cl.js} +1 -1
- package/dist/{tmux-CKdNxxJx.js → tmux-D6Ah4I8z.js} +2 -2
- package/dist/{tmux-CKdNxxJx.js.map → tmux-D6Ah4I8z.js.map} +1 -1
- package/dist/tracker-BhYYvU3p.js +198 -0
- package/dist/tracker-BhYYvU3p.js.map +1 -0
- package/dist/{tracker-utils-CVU2W1sX.js → tracker-utils-ChQyut8w.js} +34 -12
- package/dist/tracker-utils-ChQyut8w.js.map +1 -0
- package/dist/{traefik-DHgBoWXX.js → traefik-C80EbDu_.js} +4 -4
- package/dist/{traefik-DHgBoWXX.js.map → traefik-C80EbDu_.js.map} +1 -1
- package/dist/{traefik-BR-edbZv.js → traefik-CgHl7Bge.js} +1 -1
- package/dist/{tunnel-BZO9Q5oe.js → tunnel-DXOJ1wMM.js} +1 -1
- package/dist/{tunnel-Bl1qNSyQ.js → tunnel-DzXEPwIc.js} +2 -2
- package/dist/{tunnel-Bl1qNSyQ.js.map → tunnel-DzXEPwIc.js.map} +1 -1
- package/dist/{types-DewGdaIP.js → types-BhJj1SP1.js} +1 -1
- package/dist/{types-DewGdaIP.js.map → types-BhJj1SP1.js.map} +1 -1
- package/dist/{work-type-router-CS2BB1vS.js → work-type-router-CHjciPyS.js} +3 -3
- package/dist/{work-type-router-CS2BB1vS.js.map → work-type-router-CHjciPyS.js.map} +1 -1
- package/dist/{workspace-config-CNXOpKuj.js → workspace-config-fUafvYMp.js} +1 -1
- package/dist/workspace-config-fUafvYMp.js.map +1 -0
- package/dist/workspace-manager-B9jS4Dsq.js +3 -0
- package/dist/{workspace-manager-CncdZkIy.js → workspace-manager-DuLhnzJV.js} +112 -27
- package/dist/workspace-manager-DuLhnzJV.js.map +1 -0
- package/package.json +2 -1
- package/scripts/post-merge-deploy.sh +25 -5
- package/scripts/record-cost-event.js +57 -7
- package/scripts/record-cost-event.js.map +1 -1
- package/skills/pan-help/SKILL.md +1 -1
- package/skills/pan-sync/SKILL.md +6 -6
- package/skills/workspace-add-repo/skill.md +46 -0
- package/templates/claude-md/sections/warnings.md +15 -2
- package/dist/clean-planning-sZXvy3Y5.js +0 -2
- package/dist/close-issue-Dml437qV.js +0 -2
- package/dist/close-issue-Dr7yZmrr.js.map +0 -1
- package/dist/compact-beads-iu218JcO.js +0 -2
- package/dist/dashboard/agent-enrichment-C67LJBgD.js.map +0 -1
- package/dist/dashboard/clean-planning-DCu3cOTu.js +0 -2
- package/dist/dashboard/close-issue-DfIggeZD.js.map +0 -1
- package/dist/dashboard/close-issue-DwdwYtar.js +0 -2
- package/dist/dashboard/compact-beads-DXY2fK2s.js +0 -2
- package/dist/dashboard/event-store-O9q0Gweh.js.map +0 -1
- package/dist/dashboard/hume-MZndNDVU.js +0 -3
- package/dist/dashboard/label-cleanup-CZEsbtq9.js.map +0 -1
- package/dist/dashboard/lifecycle-ZTYdrr2O.js +0 -7
- package/dist/dashboard/merge-agent-twroFuAh.js.map +0 -1
- package/dist/dashboard/projects-Cq3TWdPS.js.map +0 -1
- package/dist/dashboard/providers-Ck2sQd_F.js.map +0 -1
- package/dist/dashboard/public/assets/index-CpSmB2ts.css +0 -1
- package/dist/dashboard/public/assets/index-yarWhi0M.js +0 -214
- package/dist/dashboard/rally-CQ1OBJrJ.js +0 -3
- package/dist/dashboard/settings-CuHV-wcv.js.map +0 -1
- package/dist/dashboard/settings-DMeGBRsk.js +0 -2
- package/dist/dashboard/specialists-C6s3U6tX.js.map +0 -1
- package/dist/dashboard/workflows-B2ARUpOa.js +0 -2
- package/dist/dashboard/workflows-N1UTipYl.js.map +0 -1
- package/dist/dashboard/workspace-config-cmp5_ipD.js.map +0 -1
- package/dist/dashboard/workspace-manager-D_y9ZmW_.js.map +0 -1
- package/dist/hume-BjmwmJ9E.js +0 -3
- package/dist/label-cleanup-31ElPqqv.js.map +0 -1
- package/dist/merge-agent-VQH9z9t8.js.map +0 -1
- package/dist/projects-CvLepaxC.js.map +0 -1
- package/dist/providers-DcCPZ5K4.js.map +0 -1
- package/dist/rally-DR9x8--6.js +0 -3
- package/dist/shadow-state-p3jpGRPJ.js +0 -2
- package/dist/specialists-DEKqgkxp.js.map +0 -1
- package/dist/tracker-C_62ukEq.js.map +0 -1
- package/dist/tracker-utils-CVU2W1sX.js.map +0 -1
- package/dist/workspace-config-CNXOpKuj.js.map +0 -1
- package/dist/workspace-manager-CncdZkIy.js.map +0 -1
- package/dist/workspace-manager-Cx0r2Jnv.js +0 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as __require, o as __toESM } from "./chunk-DORXReHP.js";
|
|
2
2
|
import { t as require_dist } from "./dist-C667LLmq.js";
|
|
3
|
-
import { d as createFlyProvider, f as init_fly_provider, n as init_remote_agents } from "./remote-agents-
|
|
3
|
+
import { d as createFlyProvider, f as init_fly_provider, n as init_remote_agents } from "./remote-agents-DFyjT1Le.js";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
@@ -116,4 +116,4 @@ function createFlyProviderFromConfig(remoteConfig) {
|
|
|
116
116
|
//#endregion
|
|
117
117
|
export { deleteWorkspaceMetadata as a, loadWorkspaceMetadata as c, WORKSPACES_DIR as i, saveWorkspaceMetadata as l, getRemoteProvider as n, findRemoteWorkspaceMetadata as o, isRemoteAvailable as r, listWorkspaceMetadata as s, createFlyProviderFromConfig as t };
|
|
118
118
|
|
|
119
|
-
//# sourceMappingURL=remote-
|
|
119
|
+
//# sourceMappingURL=remote-ObpNZ7hF.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-
|
|
1
|
+
{"version":3,"file":"remote-ObpNZ7hF.js","names":[],"sources":["../../src/lib/remote/workspace-metadata.ts","../../src/lib/remote/index.ts"],"sourcesContent":["/**\n * Workspace Metadata Management\n *\n * Shared module for loading, saving, and listing workspace metadata.\n * Used by both workspace.ts and work/issue.ts for remote workspace support.\n */\n\nimport { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { parse, stringify } from 'yaml';\nimport type { RemoteWorkspaceMetadata } from './interface.js';\n\n// Path for workspace metadata\nexport const WORKSPACES_DIR = join(homedir(), '.panopticon', 'workspaces');\n\n/**\n * Save workspace metadata to ~/.panopticon/workspaces/{issueId}.yaml\n */\nexport function saveWorkspaceMetadata(metadata: RemoteWorkspaceMetadata): void {\n if (!existsSync(WORKSPACES_DIR)) {\n mkdirSync(WORKSPACES_DIR, { recursive: true });\n }\n\n const filename = join(WORKSPACES_DIR, `${metadata.id}.yaml`);\n writeFileSync(filename, stringify(metadata), 'utf-8');\n}\n\n/**\n * Load workspace metadata from ~/.panopticon/workspaces/{issueId}.yaml\n */\nexport function loadWorkspaceMetadata(issueId: string): RemoteWorkspaceMetadata | null {\n const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, '-');\n const filename = join(WORKSPACES_DIR, `${normalizedId}.yaml`);\n\n if (!existsSync(filename)) {\n return null;\n }\n\n try {\n const content = readFileSync(filename, 'utf-8');\n return parse(content) as RemoteWorkspaceMetadata;\n } catch {\n return null;\n }\n}\n\n/**\n * List all workspace metadata files\n */\nexport function listWorkspaceMetadata(): RemoteWorkspaceMetadata[] {\n if (!existsSync(WORKSPACES_DIR)) {\n return [];\n }\n\n const files = readdirSync(WORKSPACES_DIR).filter(f => f.endsWith('.yaml'));\n const workspaces: RemoteWorkspaceMetadata[] = [];\n\n for (const file of files) {\n try {\n const content = readFileSync(join(WORKSPACES_DIR, file), 'utf-8');\n workspaces.push(parse(content) as RemoteWorkspaceMetadata);\n } catch {\n // Skip invalid files\n }\n }\n\n return workspaces;\n}\n\n/**\n * Check if a workspace exists (local or remote)\n * Returns metadata if remote workspace exists, null otherwise\n */\nexport function findRemoteWorkspaceMetadata(issueId: string): RemoteWorkspaceMetadata | null {\n return loadWorkspaceMetadata(issueId);\n}\n\n/**\n * Delete workspace metadata\n */\nexport function deleteWorkspaceMetadata(issueId: string): boolean {\n const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, '-');\n const filename = join(WORKSPACES_DIR, `${normalizedId}.yaml`);\n\n if (!existsSync(filename)) {\n return false;\n }\n\n try {\n const { unlinkSync } = require('fs');\n unlinkSync(filename);\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * Remote Workspace Module\n *\n * Provides support for running workspaces on remote Fly.io Machines\n * to offload Claude agents from the local machine.\n */\n\nexport type {\n RemoteProvider,\n VmInfo,\n VmStatus,\n ExecResult,\n RemoteProviderConfig,\n RemoteWorkspaceMetadata,\n} from './interface.js';\n\nexport { FlyProvider, createFlyProvider } from './fly-provider.js';\nexport type { FlyProviderConfig } from './fly-provider.js';\n\n// Remote agent management\nexport {\n spawnRemoteAgent,\n getRemoteAgentOutput,\n sendToRemoteAgent,\n isRemoteAgentRunning,\n killRemoteAgent,\n listRemoteAgents,\n pollRemoteAgentStatus,\n loadRemoteAgentState,\n} from './remote-agents.js';\nexport type { RemoteAgentState, SpawnRemoteAgentOptions } from './remote-agents.js';\n\n// Workspace metadata management\nexport {\n saveWorkspaceMetadata,\n loadWorkspaceMetadata,\n listWorkspaceMetadata,\n deleteWorkspaceMetadata,\n findRemoteWorkspaceMetadata,\n WORKSPACES_DIR,\n} from './workspace-metadata.js';\n\nimport { FlyProvider, createFlyProvider } from './fly-provider.js';\nimport type { RemoteProvider, RemoteProviderConfig } from './interface.js';\n\nexport type ProviderType = 'fly';\n\n/**\n * Get a remote provider by type\n */\nexport function getRemoteProvider(\n type: ProviderType,\n config?: RemoteProviderConfig\n): RemoteProvider {\n switch (type) {\n case 'fly':\n return createFlyProvider();\n default:\n throw new Error(`Unknown remote provider type: ${type}`);\n }\n}\n\n/**\n * Check if remote providers are available\n */\nexport async function isRemoteAvailable(): Promise<{ available: boolean; reason?: string }> {\n const fly = createFlyProvider();\n\n try {\n const isAuth = await fly.isAuthenticated();\n if (!isAuth) {\n return {\n available: false,\n reason: 'Not authenticated with Fly.io. Set FLY_API_TOKEN or run: fly auth login',\n };\n }\n return { available: true };\n } catch (error: any) {\n return {\n available: false,\n reason: `Fly.io not available: ${error.message}`,\n };\n }\n}\n\n/**\n * Create a FlyProvider from config settings\n */\nexport function createFlyProviderFromConfig(remoteConfig?: {\n fly?: {\n app?: string;\n org?: string;\n region?: string;\n vm_size?: string;\n vm_memory?: number;\n image?: string;\n api_token_env?: string;\n };\n}): FlyProvider {\n const fly = remoteConfig?.fly;\n const tokenEnv = fly?.api_token_env ?? 'FLY_API_TOKEN';\n return createFlyProvider({\n app: fly?.app,\n org: fly?.org,\n region: fly?.region,\n vmSize: fly?.vm_size,\n vmMemory: fly?.vm_memory,\n image: fly?.image,\n apiToken: process.env[tokenEnv],\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAa,iBAAiB,KAAK,SAAS,EAAE,eAAe,aAAa;;;;AAK1E,SAAgB,sBAAsB,UAAyC;AAC7E,KAAI,CAAC,WAAW,eAAe,CAC7B,WAAU,gBAAgB,EAAE,WAAW,MAAM,CAAC;AAIhD,eADiB,KAAK,gBAAgB,GAAG,SAAS,GAAG,OAAO,GAAA,GAAA,YAAA,WAC1B,SAAS,EAAE,QAAQ;;;;;AAMvD,SAAgB,sBAAsB,SAAiD;CAErF,MAAM,WAAW,KAAK,gBAAgB,GADjB,QAAQ,aAAa,CAAC,QAAQ,eAAe,IAAI,CAChB,OAAO;AAE7D,KAAI,CAAC,WAAW,SAAS,CACvB,QAAO;AAGT,KAAI;AAEF,UAAA,GAAA,YAAA,OADgB,aAAa,UAAU,QAAQ,CAC1B;SACf;AACN,SAAO;;;;;;AAOX,SAAgB,wBAAmD;AACjE,KAAI,CAAC,WAAW,eAAe,CAC7B,QAAO,EAAE;CAGX,MAAM,QAAQ,YAAY,eAAe,CAAC,QAAO,MAAK,EAAE,SAAS,QAAQ,CAAC;CAC1E,MAAM,aAAwC,EAAE;AAEhD,MAAK,MAAM,QAAQ,MACjB,KAAI;EACF,MAAM,UAAU,aAAa,KAAK,gBAAgB,KAAK,EAAE,QAAQ;AACjE,aAAW,MAAA,GAAA,YAAA,OAAW,QAAQ,CAA4B;SACpD;AAKV,QAAO;;;;;;AAOT,SAAgB,4BAA4B,SAAiD;AAC3F,QAAO,sBAAsB,QAAQ;;;;;AAMvC,SAAgB,wBAAwB,SAA0B;CAEhE,MAAM,WAAW,KAAK,gBAAgB,GADjB,QAAQ,aAAa,CAAC,QAAQ,eAAe,IAAI,CAChB,OAAO;AAE7D,KAAI,CAAC,WAAW,SAAS,CACvB,QAAO;AAGT,KAAI;EACF,MAAM,EAAE,eAAA,UAAuB,KAAK;AACpC,aAAW,SAAS;AACpB,SAAO;SACD;AACN,SAAO;;;;;mBC9EwD;oBAavC;;;;AAqB5B,SAAgB,kBACd,MACA,QACgB;AAChB,SAAQ,MAAR;EACE,KAAK,MACH,QAAO,mBAAmB;EAC5B,QACE,OAAM,IAAI,MAAM,iCAAiC,OAAO;;;;;;AAO9D,eAAsB,oBAAsE;CAC1F,MAAM,MAAM,mBAAmB;AAE/B,KAAI;AAEF,MAAI,CADW,MAAM,IAAI,iBAAiB,CAExC,QAAO;GACL,WAAW;GACX,QAAQ;GACT;AAEH,SAAO,EAAE,WAAW,MAAM;UACnB,OAAY;AACnB,SAAO;GACL,WAAW;GACX,QAAQ,yBAAyB,MAAM;GACxC;;;;;;AAOL,SAAgB,4BAA4B,cAU5B;CACd,MAAM,MAAM,cAAc;CAC1B,MAAM,WAAW,KAAK,iBAAiB;AACvC,QAAO,kBAAkB;EACvB,KAAK,KAAK;EACV,KAAK,KAAK;EACV,QAAQ,KAAK;EACb,QAAQ,KAAK;EACb,UAAU,KAAK;EACf,OAAO,KAAK;EACZ,UAAU,QAAQ,IAAI;EACvB,CAAC"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as listRemoteAgents, c as sendToRemoteAgent, i as killRemoteAgent, l as spawnRemoteAgent, n as init_remote_agents, o as loadRemoteAgentState, r as isRemoteAgentRunning, s as pollRemoteAgentStatus, t as getRemoteAgentOutput } from "./remote-agents-
|
|
1
|
+
import { a as listRemoteAgents, c as sendToRemoteAgent, i as killRemoteAgent, l as spawnRemoteAgent, n as init_remote_agents, o as loadRemoteAgentState, r as isRemoteAgentRunning, s as pollRemoteAgentStatus, t as getRemoteAgentOutput } from "./remote-agents-DFyjT1Le.js";
|
|
2
2
|
init_remote_agents();
|
|
3
3
|
export { getRemoteAgentOutput, isRemoteAgentRunning, killRemoteAgent, listRemoteAgents, loadRemoteAgentState, pollRemoteAgentStatus, sendToRemoteAgent, spawnRemoteAgent };
|
|
@@ -605,4 +605,4 @@ var init_remote_agents = __esmMin((() => {
|
|
|
605
605
|
//#endregion
|
|
606
606
|
export { listRemoteAgents as a, sendToRemoteAgent as c, createFlyProvider as d, init_fly_provider as f, killRemoteAgent as i, spawnRemoteAgent as l, init_remote_agents as n, loadRemoteAgentState as o, isRemoteAgentRunning as r, pollRemoteAgentStatus as s, getRemoteAgentOutput as t, FlyProvider as u };
|
|
607
607
|
|
|
608
|
-
//# sourceMappingURL=remote-agents-
|
|
608
|
+
//# sourceMappingURL=remote-agents-DFyjT1Le.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-agents-C0_0LLNd.js","names":[],"sources":["../../src/lib/remote/fly-api.ts","../../src/lib/remote/fly-provider.ts","../../src/lib/remote/remote-agents.ts"],"sourcesContent":["/**\n * Fly Machines REST API Client\n *\n * Wraps the Fly Machines API (flaps) for machine lifecycle management.\n * Base URL: https://api.machines.dev/v1\n * Auth: FLY_API_TOKEN environment variable\n */\n\nexport interface FlyMachineConfig {\n image: string;\n env?: Record<string, string>;\n size?: string; // e.g. \"shared-cpu-2x\"\n memory?: number; // MB\n region?: string; // e.g. \"iad\"\n auto_destroy?: boolean;\n restart?: { policy: 'no' | 'always' | 'on-failure' };\n metadata?: Record<string, string>;\n}\n\nexport interface FlyMachine {\n id: string;\n name: string;\n state: string; // 'started', 'stopped', 'created', 'destroying', etc.\n region: string;\n image_ref?: { registry: string; repository: string; tag: string };\n instance_id?: string;\n private_ip?: string;\n created_at?: string;\n config?: {\n image: string;\n env?: Record<string, string>;\n guest?: { cpu_kind: string; cpus: number; memory_mb: number };\n };\n}\n\nexport interface FlyExecResult {\n stdout: string;\n stderr: string;\n exit_code: number;\n}\n\nexport class FlyApiError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly body: string\n ) {\n super(message);\n this.name = 'FlyApiError';\n }\n}\n\nconst BASE_URL = 'https://api.machines.dev/v1';\n\nexport class FlyApiClient {\n private readonly token: string;\n\n constructor(token: string) {\n this.token = token;\n }\n\n private async request<T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> {\n const url = `${BASE_URL}${path}`;\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n };\n\n const response = await fetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const text = await response.text();\n\n if (!response.ok) {\n throw new FlyApiError(\n `Fly API error ${response.status} for ${method} ${path}: ${text}`,\n response.status,\n text\n );\n }\n\n if (!text) return undefined as T;\n\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n }\n\n /** Create a machine in an app */\n async createMachine(\n appName: string,\n name: string,\n config: FlyMachineConfig\n ): Promise<FlyMachine> {\n return this.request<FlyMachine>('POST', `/apps/${appName}/machines`, {\n name,\n config: {\n image: config.image,\n env: config.env,\n guest: config.size\n ? { cpu_kind: 'shared', cpus: 2, memory_mb: config.memory ?? 1024 }\n : undefined,\n restart: config.restart ?? { policy: 'no' },\n auto_destroy: config.auto_destroy,\n metadata: config.metadata,\n },\n region: config.region,\n });\n }\n\n /** Destroy a machine (force=true for immediate) */\n async destroyMachine(appName: string, machineId: string): Promise<void> {\n await this.request<void>(\n 'DELETE',\n `/apps/${appName}/machines/${machineId}?force=true`\n );\n }\n\n /** Start a stopped machine */\n async startMachine(appName: string, machineId: string): Promise<void> {\n await this.request<void>(\n 'POST',\n `/apps/${appName}/machines/${machineId}/start`\n );\n }\n\n /** Stop a running machine */\n async stopMachine(\n appName: string,\n machineId: string,\n signal?: string,\n timeout?: number\n ): Promise<void> {\n await this.request<void>(\n 'POST',\n `/apps/${appName}/machines/${machineId}/stop`,\n signal || timeout ? { signal, timeout } : undefined\n );\n }\n\n /** Get a machine by ID */\n async getMachine(appName: string, machineId: string): Promise<FlyMachine> {\n return this.request<FlyMachine>(\n 'GET',\n `/apps/${appName}/machines/${machineId}`\n );\n }\n\n /** List all machines in an app */\n async listMachines(appName: string): Promise<FlyMachine[]> {\n const result = await this.request<FlyMachine[] | null>(\n 'GET',\n `/apps/${appName}/machines`\n );\n return result ?? [];\n }\n\n /** Execute a command inside a running machine */\n async execCommand(\n appName: string,\n machineId: string,\n command: string[],\n timeout: number = 30\n ): Promise<FlyExecResult> {\n return this.request<FlyExecResult>(\n 'POST',\n `/apps/${appName}/machines/${machineId}/exec`,\n { command, timeout }\n );\n }\n\n /** Wait for a machine to reach a target state */\n async waitForState(\n appName: string,\n machineId: string,\n state: string,\n timeout: number = 60\n ): Promise<void> {\n await this.request<void>(\n 'GET',\n `/apps/${appName}/machines/${machineId}/wait?state=${state}&timeout=${timeout}`\n );\n }\n\n /** Create a Fly app if it doesn't exist */\n async ensureApp(appName: string, orgSlug: string): Promise<void> {\n try {\n await this.request<unknown>('GET', `/apps/${appName}`);\n } catch (err) {\n if (err instanceof FlyApiError && err.statusCode === 404) {\n await this.request<unknown>('POST', '/apps', {\n app_name: appName,\n org_slug: orgSlug,\n network: 'default',\n });\n } else {\n throw err;\n }\n }\n }\n}\n\n/** Create a FlyApiClient from env or explicit token */\nexport function createFlyApiClient(token?: string): FlyApiClient {\n const tok = token ?? process.env.FLY_API_TOKEN;\n if (!tok) {\n throw new Error(\n 'Fly API token not found. Set FLY_API_TOKEN environment variable or run: fly auth login'\n );\n }\n return new FlyApiClient(tok);\n}\n","/**\n * Fly.io Remote Provider\n *\n * Implements the RemoteProvider interface using Fly Machines API and Fly CLI.\n * VM lifecycle is managed via REST API; exec/SSH via Fly CLI.\n */\n\nimport { exec, spawn } from 'child_process';\nimport { promisify } from 'util';\nimport { existsSync, readFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { parse } from 'yaml';\nimport { FlyApiClient, createFlyApiClient, FlyApiError } from './fly-api.js';\nimport type { RemoteProvider, VmInfo, VmStatus, ExecResult } from './interface.js';\n\nconst execAsync = promisify(exec);\n\nexport interface FlyProviderConfig {\n /** Fly.io app name for workspace machines (default: pan-workspaces) */\n app?: string;\n /** Fly.io org slug */\n org?: string;\n /** Default region (default: iad) */\n region?: string;\n /** Machine size (default: shared-cpu-2x) */\n vmSize?: string;\n /** Memory in MB (default: 1024) */\n vmMemory?: number;\n /** Docker image for workspace machines */\n image?: string;\n /** API token (falls back to FLY_API_TOKEN env var) */\n apiToken?: string;\n}\n\nfunction mapFlyStateToVmStatus(state: string): VmStatus {\n switch (state) {\n case 'started':\n return 'running';\n case 'stopped':\n case 'suspended':\n return 'stopped';\n case 'created':\n case 'replacing':\n return 'creating';\n case 'destroying':\n case 'destroyed':\n return 'deleting';\n default:\n return 'unknown';\n }\n}\n\nexport class FlyProvider implements RemoteProvider {\n readonly name = 'fly';\n\n private readonly config: Required<FlyProviderConfig>;\n private api: FlyApiClient | null = null;\n\n constructor(config: FlyProviderConfig = {}) {\n this.config = {\n app: config.app ?? 'pan-workspaces',\n org: config.org ?? 'personal',\n region: config.region ?? 'iad',\n vmSize: config.vmSize ?? 'shared-cpu-2x',\n vmMemory: config.vmMemory ?? 1024,\n image: config.image ?? 'registry.fly.io/pan-workspace:latest',\n apiToken: config.apiToken ?? process.env.FLY_API_TOKEN ?? '',\n };\n }\n\n private getApi(): FlyApiClient {\n if (!this.api) {\n this.api = createFlyApiClient(this.config.apiToken || undefined);\n }\n return this.api;\n }\n\n async isAuthenticated(): Promise<boolean> {\n // Check API token first\n if (this.config.apiToken || process.env.FLY_API_TOKEN) {\n try {\n await this.getApi().listMachines(this.config.app);\n return true;\n } catch {\n // Fall through to CLI check\n }\n }\n\n // Check fly CLI auth\n try {\n const result = await execAsync('fly auth whoami', { timeout: 10000 });\n return !result.stdout.includes('not logged in');\n } catch {\n return false;\n }\n }\n\n /**\n * Resolve vmName to {appName, machineId} by scanning workspace metadata.\n * Falls back to listing machines in the app.\n */\n async resolveVm(vmName: string): Promise<{ appName: string; machineId: string }> {\n const workspacesDir = join(homedir(), '.panopticon', 'workspaces');\n if (existsSync(workspacesDir)) {\n for (const file of readdirSync(workspacesDir)) {\n if (!file.endsWith('.yaml')) continue;\n try {\n const content = readFileSync(join(workspacesDir, file), 'utf-8');\n const metadata = parse(content) as { vmName?: string; machineId?: string; appName?: string };\n if (metadata.vmName === vmName && metadata.machineId && metadata.appName) {\n return { appName: metadata.appName, machineId: metadata.machineId };\n }\n } catch {\n // Skip invalid files\n }\n }\n }\n\n // Fallback: search by machine name via API\n const machines = await this.getApi().listMachines(this.config.app);\n const machine = machines.find(m => m.name === vmName);\n if (!machine) {\n throw new Error(`No Fly machine found for VM name: ${vmName}`);\n }\n return { appName: this.config.app, machineId: machine.id };\n }\n\n async createVm(name: string): Promise<VmInfo> {\n const api = this.getApi();\n\n // Ensure app exists\n await api.ensureApp(this.config.app, this.config.org);\n\n // Create machine\n const machine = await api.createMachine(this.config.app, name, {\n image: this.config.image,\n size: this.config.vmSize,\n memory: this.config.vmMemory,\n region: this.config.region,\n restart: { policy: 'no' },\n auto_destroy: false,\n });\n\n // Wait for machine to start\n try {\n await api.waitForState(this.config.app, machine.id, 'started', 120);\n } catch {\n // Non-fatal: machine may still be starting\n }\n\n return {\n name,\n status: mapFlyStateToVmStatus(machine.state),\n machineId: machine.id,\n ipAddress: machine.private_ip,\n created: machine.created_at ? new Date(machine.created_at) : undefined,\n };\n }\n\n async deleteVm(name: string): Promise<void> {\n const { appName, machineId } = await this.resolveVm(name);\n await this.getApi().destroyMachine(appName, machineId);\n }\n\n async listVms(): Promise<VmInfo[]> {\n const machines = await this.getApi().listMachines(this.config.app);\n return machines.map(m => ({\n name: m.name,\n status: mapFlyStateToVmStatus(m.state),\n machineId: m.id,\n ipAddress: m.private_ip,\n created: m.created_at ? new Date(m.created_at) : undefined,\n }));\n }\n\n async getStatus(name: string): Promise<VmStatus> {\n try {\n const { appName, machineId } = await this.resolveVm(name);\n const machine = await this.getApi().getMachine(appName, machineId);\n return mapFlyStateToVmStatus(machine.state);\n } catch {\n return 'unknown';\n }\n }\n\n async getVmInfo(name: string): Promise<VmInfo | null> {\n try {\n const { appName, machineId } = await this.resolveVm(name);\n const machine = await this.getApi().getMachine(appName, machineId);\n return {\n name,\n status: mapFlyStateToVmStatus(machine.state),\n machineId: machine.id,\n ipAddress: machine.private_ip,\n created: machine.created_at ? new Date(machine.created_at) : undefined,\n memoryTotal: machine.config?.guest?.memory_mb,\n };\n } catch {\n return null;\n }\n }\n\n async startVm(name: string): Promise<void> {\n const { appName, machineId } = await this.resolveVm(name);\n await this.getApi().startMachine(appName, machineId);\n await this.getApi().waitForState(appName, machineId, 'started', 60);\n }\n\n async stopVm(name: string): Promise<void> {\n const { appName, machineId } = await this.resolveVm(name);\n await this.getApi().stopMachine(appName, machineId);\n }\n\n /** Execute a command on the VM via Fly Machines exec API */\n async ssh(vm: string, command: string): Promise<ExecResult> {\n const { appName, machineId } = await this.resolveVm(vm);\n try {\n const result = await this.getApi().execCommand(\n appName,\n machineId,\n ['/bin/sh', '-c', command],\n 60\n );\n return {\n stdout: result.stdout ?? '',\n stderr: result.stderr ?? '',\n exitCode: result.exit_code ?? 0,\n };\n } catch (err) {\n if (err instanceof FlyApiError) {\n return { stdout: '', stderr: err.message, exitCode: 1 };\n }\n throw err;\n }\n }\n\n /** Stream command output via fly SSH console */\n async *sshStream(vm: string, command: string): AsyncIterable<string> {\n const { appName } = await this.resolveVm(vm);\n const child = spawn('fly', ['ssh', 'console', '-a', appName, '-C', command], {\n env: { ...process.env },\n });\n\n for await (const chunk of child.stdout) {\n yield chunk.toString();\n }\n for await (const chunk of child.stderr) {\n yield chunk.toString();\n }\n\n await new Promise<void>((resolve, reject) => {\n child.on('close', resolve);\n child.on('error', reject);\n });\n }\n\n /** Copy a local file to VM using base64 encoding */\n async copyToVm(vm: string, localPath: string, remotePath: string): Promise<void> {\n const content = readFileSync(localPath);\n const b64 = content.toString('base64');\n const dirPath = remotePath.substring(0, remotePath.lastIndexOf('/'));\n if (dirPath) {\n await this.ssh(vm, `mkdir -p ${JSON.stringify(dirPath)}`);\n }\n await this.ssh(vm, `echo '${b64}' | base64 -d > ${JSON.stringify(remotePath)}`);\n }\n\n /** Copy a file from VM to local path */\n async copyFromVm(vm: string, remotePath: string, localPath: string): Promise<void> {\n const { appName } = await this.resolveVm(vm);\n await execAsync(\n `fly ssh sftp get -a ${JSON.stringify(appName)} ${JSON.stringify(remotePath)} ${JSON.stringify(localPath)}`,\n { timeout: 60000 }\n );\n }\n\n /** Expose a port — not supported by Fly.io provider */\n async exposePort(_vm: string, _port: number): Promise<string> {\n throw new Error(\n 'exposePort is not supported by the Fly.io provider. ' +\n 'Configure services in fly.toml or via the Fly Machines API config.'\n );\n }\n\n /** Create a fly proxy tunnel to the machine */\n async tunnel(\n vm: string,\n remotePort: number,\n localPort: number\n ): Promise<{ close: () => void }> {\n const { appName } = await this.resolveVm(vm);\n const child = spawn('fly', ['proxy', `${localPort}:${remotePort}`, '-a', appName], {\n env: { ...process.env },\n });\n\n return {\n close: () => {\n child.kill();\n },\n };\n }\n\n // ============================================================================\n // Credential Sync & Configuration (ported from ExeProvider)\n // ============================================================================\n\n /** Sync Claude Code credentials from local macOS Keychain to remote VM */\n async syncClaudeCredentials(vmName: string): Promise<boolean> {\n try {\n const { stdout: credentials } = await execAsync(\n 'security find-generic-password -s \"Claude Code-credentials\" -w 2>/dev/null',\n { encoding: 'utf-8', timeout: 10000 }\n );\n if (!credentials?.trim()) return false;\n\n const b64 = Buffer.from(credentials.trim()).toString('base64');\n await this.ssh(vmName, `mkdir -p ~/.claude && echo '${b64}' | base64 -d > ~/.claude/.credentials.json`);\n return true;\n } catch {\n return false;\n }\n }\n\n /** Sync GitHub CLI authentication to the remote VM */\n async syncGitHubAuth(vmName: string): Promise<boolean> {\n const ghConfigPath = join(homedir(), '.config', 'gh', 'hosts.yml');\n if (!existsSync(ghConfigPath)) return false;\n\n try {\n const content = readFileSync(ghConfigPath, 'utf-8');\n const b64 = Buffer.from(content).toString('base64');\n await this.ssh(vmName, `mkdir -p ~/.config/gh && echo '${b64}' | base64 -d > ~/.config/gh/hosts.yml`);\n return true;\n } catch {\n return false;\n }\n }\n\n /** Sync GitLab CLI (glab) authentication to the remote VM */\n async syncGitLabAuth(vmName: string): Promise<boolean> {\n const glabConfigPath = join(homedir(), '.config', 'glab-cli', 'config.yml');\n if (!existsSync(glabConfigPath)) return false;\n\n try {\n const content = readFileSync(glabConfigPath, 'utf-8');\n const b64 = Buffer.from(content).toString('base64');\n await this.ssh(vmName, `mkdir -p ~/.config/glab-cli && echo '${b64}' | base64 -d > ~/.config/glab-cli/config.yml`);\n return true;\n } catch {\n return false;\n }\n }\n\n /** Sync all credentials needed for remote workspace operation */\n async syncAllCredentials(vmName: string): Promise<{ claude: boolean; github: boolean }> {\n const [claude, github] = await Promise.all([\n this.syncClaudeCredentials(vmName),\n this.syncGitHubAuth(vmName),\n ]);\n return { claude, github };\n }\n\n /** Install beads CLI (bd) on a remote VM */\n async installBeads(vmName: string): Promise<boolean> {\n // Check if already installed\n const check = await this.ssh(vmName, 'which bd 2>/dev/null');\n if (check.exitCode === 0 && check.stdout.trim()) return true;\n\n // Install via npm\n const result = await this.ssh(vmName, 'npm install -g @beads-dev/beads 2>&1');\n if (result.exitCode !== 0) {\n // Try alternative install\n const alt = await this.ssh(\n vmName,\n 'curl -fsSL https://raw.githubusercontent.com/beads-dev/beads/main/install.sh | bash 2>&1'\n );\n return alt.exitCode === 0;\n }\n return true;\n }\n\n /** Initialize beads in a workspace on a remote VM */\n async initBeads(vmName: string, workspacePath: string = '/workspace'): Promise<boolean> {\n const result = await this.ssh(\n vmName,\n `cd ${workspacePath} && (bd init --prefix PAN 2>&1 || bd init 2>&1) && git config beads.role contributor`\n );\n return result.exitCode === 0;\n }\n\n /** Configure Claude Code on a VM for autonomous operation */\n async configureClaudeCode(vmName: string): Promise<void> {\n await this.ssh(vmName, 'mkdir -p ~/.claude');\n\n // Set onboarding complete\n const onboardingScript = `\nimport json, os\npath = os.path.expanduser(\"~/.claude.json\")\ndata = {}\nif os.path.exists(path):\n with open(path) as f:\n data = json.load(f)\ndata[\"hasCompletedOnboarding\"] = True\ndata[\"lastOnboardingVersion\"] = \"2.0.50\"\nwith open(path, \"w\") as f:\n json.dump(data, f, indent=2)\n`;\n const scriptB64 = Buffer.from(onboardingScript).toString('base64');\n await this.ssh(vmName, `echo '${scriptB64}' | base64 -d | python3`);\n\n // Write settings.json with bypass permissions\n const settings = JSON.stringify({\n theme: 'dark',\n permissions: { defaultMode: 'bypassPermissions' },\n });\n const settingsB64 = Buffer.from(settings).toString('base64');\n await this.ssh(vmName, `echo '${settingsB64}' | base64 -d > ~/.claude/settings.json`);\n }\n\n /** Copy essential skills from local ~/.panopticon/skills/ to remote VM */\n async copySkillsToVm(vmName: string): Promise<void> {\n const skillsDir = join(homedir(), '.panopticon', 'skills');\n if (!existsSync(skillsDir)) return;\n\n await this.ssh(vmName, 'mkdir -p ~/.claude/skills');\n\n try {\n const entries = readdirSync(skillsDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.md')) continue;\n const localPath = join(skillsDir, entry.name);\n const content = readFileSync(localPath, 'utf-8');\n const b64 = Buffer.from(content).toString('base64');\n await this.ssh(vmName, `echo '${b64}' | base64 -d > ~/.claude/skills/${entry.name}`);\n }\n } catch {\n // Non-fatal: skills are optional\n }\n }\n\n /** Sync beads from remote VM to git: exports JSONL, commits, and pushes */\n async syncBeadsToGit(\n vmName: string,\n workspacePath: string = '/workspace',\n commitMessage?: string\n ): Promise<boolean> {\n const msg = commitMessage ?? 'chore: sync beads from remote';\n\n // Export beads to JSONL\n const exportResult = await this.ssh(\n vmName,\n `cd ${workspacePath} && bd export --output .beads/issues.jsonl 2>&1`\n );\n if (exportResult.exitCode !== 0) {\n return false;\n }\n\n // Commit and push\n const gitResult = await this.ssh(\n vmName,\n `cd ${workspacePath} && git diff --cached --quiet || (git commit -m ${JSON.stringify(msg)} && git push origin HEAD) 2>&1`\n );\n return gitResult.exitCode === 0;\n }\n\n /** Query beads on a remote VM via bd search */\n async queryBeads(\n vmName: string,\n searchTerm: string,\n workspacePath: string = '/workspace'\n ): Promise<unknown[]> {\n const result = await this.ssh(\n vmName,\n `cd ${workspacePath} && bd search ${JSON.stringify(searchTerm)} --json 2>/dev/null || echo '[]'`\n );\n try {\n return JSON.parse(result.stdout.trim() || '[]');\n } catch {\n return [];\n }\n }\n\n /** Get the configured app name */\n getAppName(): string {\n return this.config.app;\n }\n}\n\nexport function createFlyProvider(config?: FlyProviderConfig): FlyProvider {\n return new FlyProvider(config);\n}\n","/**\n * Remote Agent Management\n *\n * Spawn and manage Claude agents on remote Fly.io machines.\n * Agents run in tmux sessions for persistence and monitoring.\n */\n\nimport { createFlyProvider } from './fly-provider.js';\nimport type { RemoteProvider, RemoteWorkspaceMetadata } from './interface.js';\nimport { join } from 'path';\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { homedir } from 'os';\n\nconst AGENTS_DIR = join(homedir(), '.panopticon', 'agents');\n\nexport interface RemoteAgentState {\n id: string;\n issueId: string;\n vmName: string;\n model: string;\n status: 'starting' | 'running' | 'stopped' | 'error';\n startedAt: string;\n lastActivity?: string;\n location: 'remote';\n}\n\n/**\n * Get agent state file path\n */\nfunction getRemoteAgentStateFile(agentId: string): string {\n return join(AGENTS_DIR, agentId, 'remote-state.json');\n}\n\n/**\n * Save remote agent state\n */\nfunction saveRemoteAgentState(state: RemoteAgentState): void {\n const dir = join(AGENTS_DIR, state.id);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(getRemoteAgentStateFile(state.id), JSON.stringify(state, null, 2));\n}\n\n/**\n * Load remote agent state\n */\nexport function loadRemoteAgentState(agentId: string): RemoteAgentState | null {\n const file = getRemoteAgentStateFile(agentId);\n if (!existsSync(file)) return null;\n\n try {\n return JSON.parse(readFileSync(file, 'utf-8'));\n } catch {\n return null;\n }\n}\n\n/**\n * Check if remote agent session exists\n */\nasync function remoteSessionExists(\n provider: RemoteProvider,\n vmName: string,\n sessionName: string\n): Promise<boolean> {\n const result = await provider.ssh(vmName, `tmux has-session -t ${sessionName} 2>/dev/null && echo exists || echo not-found`);\n return result.stdout.trim() === 'exists';\n}\n\nexport interface SpawnRemoteAgentOptions {\n issueId: string;\n workspace: RemoteWorkspaceMetadata;\n model?: string;\n prompt?: string;\n phase?: string;\n}\n\n/**\n * Spawn a Claude agent on a remote VM\n */\nexport async function spawnRemoteAgent(options: SpawnRemoteAgentOptions): Promise<RemoteAgentState> {\n const { issueId, workspace, model = 'claude-sonnet-4-6', prompt } = options;\n\n const agentId = `agent-${issueId.toLowerCase()}`;\n const vmName = workspace.vmName;\n\n const fly = createFlyProvider();\n\n // Check if VM is running\n const vmStatus = await fly.getStatus(vmName);\n if (vmStatus !== 'running') {\n throw new Error(`VM ${vmName} is not running. Start it with: pan workspace start ${issueId}`);\n }\n\n // Check if agent already exists\n if (await remoteSessionExists(fly, vmName, agentId)) {\n throw new Error(`Agent ${agentId} already running on ${vmName}. Use 'pan work tell' to message it.`);\n }\n\n // Create agent state\n const state: RemoteAgentState = {\n id: agentId,\n issueId,\n vmName,\n model,\n status: 'starting',\n startedAt: new Date().toISOString(),\n location: 'remote',\n };\n\n saveRemoteAgentState(state);\n\n // Write prompt to file on remote VM if provided\n let claudeCmd: string;\n\n if (prompt) {\n // Write prompt to file on VM using base64 to avoid escaping issues\n const promptFile = `/workspace/.pan/prompts/${agentId}.md`;\n await fly.ssh(vmName, `mkdir -p /workspace/.pan/prompts`);\n const promptBase64 = Buffer.from(prompt).toString('base64');\n await fly.ssh(vmName, `echo '${promptBase64}' | base64 -d > ${promptFile}`);\n\n // Create launcher script using base64 to avoid shell interpretation\n const launcherScript = `/workspace/.pan/prompts/${agentId}-launcher.sh`;\n const launcherContent = `#!/bin/bash\nexport PATH=\"/usr/local/bin:\\$PATH\"\nprompt=\\$(cat \"${promptFile}\")\nexec claude --dangerously-skip-permissions --model ${model} \"\\$prompt\"\n`;\n const launcherBase64 = Buffer.from(launcherContent).toString('base64');\n await fly.ssh(vmName, `echo '${launcherBase64}' | base64 -d > ${launcherScript} && chmod +x ${launcherScript}`);\n\n claudeCmd = `bash ${launcherScript}`;\n } else {\n claudeCmd = `claude --dangerously-skip-permissions --model ${model}`;\n }\n\n // Create tmux session on remote VM\n const tmuxCmd = `tmux new-session -d -s ${agentId} -c /workspace '${claudeCmd}'`;\n const result = await fly.ssh(vmName, tmuxCmd);\n\n if (result.exitCode !== 0) {\n state.status = 'error';\n saveRemoteAgentState(state);\n throw new Error(`Failed to start agent: ${result.stderr}`);\n }\n\n // Update status\n state.status = 'running';\n saveRemoteAgentState(state);\n\n return state;\n}\n\n/**\n * Get remote agent output from tmux session\n */\nexport async function getRemoteAgentOutput(\n agentId: string,\n vmName: string,\n lines: number = 100\n): Promise<string> {\n const fly = createFlyProvider();\n\n const result = await fly.ssh(vmName, `tmux capture-pane -t ${agentId} -p -S -${lines}`);\n return result.stdout;\n}\n\n/**\n * Send message to remote agent\n */\nexport async function sendToRemoteAgent(\n agentId: string,\n vmName: string,\n message: string\n): Promise<void> {\n const fly = createFlyProvider();\n\n // Escape message for shell\n const escapedMessage = message.replace(/'/g, \"'\\\\''\");\n\n // Send keys to tmux session (send message then Enter)\n await fly.ssh(vmName, `tmux send-keys -t ${agentId} '${escapedMessage}'`);\n await fly.ssh(vmName, `tmux send-keys -t ${agentId} C-m`);\n}\n\n/**\n * Check if remote agent is still running\n */\nexport async function isRemoteAgentRunning(\n agentId: string,\n vmName: string\n): Promise<boolean> {\n const fly = createFlyProvider();\n return remoteSessionExists(fly, vmName, agentId);\n}\n\n/**\n * Kill remote agent session\n */\nexport async function killRemoteAgent(\n agentId: string,\n vmName: string\n): Promise<void> {\n const fly = createFlyProvider();\n await fly.ssh(vmName, `tmux kill-session -t ${agentId} 2>/dev/null || true`);\n\n // Update state\n const state = loadRemoteAgentState(agentId);\n if (state) {\n state.status = 'stopped';\n saveRemoteAgentState(state);\n }\n}\n\n/**\n * Get list of running remote agents on a VM\n */\nexport async function listRemoteAgents(vmName: string): Promise<string[]> {\n const fly = createFlyProvider();\n\n const result = await fly.ssh(vmName, `tmux list-sessions -F \"#{session_name}\" 2>/dev/null | grep \"^agent-\" || true`);\n if (!result.stdout.trim()) {\n return [];\n }\n\n return result.stdout.trim().split('\\n').filter(Boolean);\n}\n\n/**\n * Poll remote agent for status updates\n * Returns parsed events from the agent output\n */\nexport async function pollRemoteAgentStatus(\n agentId: string,\n vmName: string\n): Promise<{\n isRunning: boolean;\n lastOutput: string;\n toolUses: string[];\n}> {\n const fly = createFlyProvider();\n\n // Check if session exists\n const isRunning = await remoteSessionExists(fly, vmName, agentId);\n\n if (!isRunning) {\n return { isRunning: false, lastOutput: '', toolUses: [] };\n }\n\n // Get recent output\n const output = await getRemoteAgentOutput(agentId, vmName, 50);\n\n // Parse tool uses from output (simple pattern matching)\n const toolUses: string[] = [];\n const toolPattern = /(?:Using|Calling|Running)\\s+(\\w+)\\s+tool/gi;\n let match;\n while ((match = toolPattern.exec(output)) !== null) {\n toolUses.push(match[1]);\n }\n\n return {\n isRunning,\n lastOutput: output,\n toolUses,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"remote-agents-DFyjT1Le.js","names":[],"sources":["../../src/lib/remote/fly-api.ts","../../src/lib/remote/fly-provider.ts","../../src/lib/remote/remote-agents.ts"],"sourcesContent":["/**\n * Fly Machines REST API Client\n *\n * Wraps the Fly Machines API (flaps) for machine lifecycle management.\n * Base URL: https://api.machines.dev/v1\n * Auth: FLY_API_TOKEN environment variable\n */\n\nexport interface FlyMachineConfig {\n image: string;\n env?: Record<string, string>;\n size?: string; // e.g. \"shared-cpu-2x\"\n memory?: number; // MB\n region?: string; // e.g. \"iad\"\n auto_destroy?: boolean;\n restart?: { policy: 'no' | 'always' | 'on-failure' };\n metadata?: Record<string, string>;\n}\n\nexport interface FlyMachine {\n id: string;\n name: string;\n state: string; // 'started', 'stopped', 'created', 'destroying', etc.\n region: string;\n image_ref?: { registry: string; repository: string; tag: string };\n instance_id?: string;\n private_ip?: string;\n created_at?: string;\n config?: {\n image: string;\n env?: Record<string, string>;\n guest?: { cpu_kind: string; cpus: number; memory_mb: number };\n };\n}\n\nexport interface FlyExecResult {\n stdout: string;\n stderr: string;\n exit_code: number;\n}\n\nexport class FlyApiError extends Error {\n constructor(\n message: string,\n public readonly statusCode: number,\n public readonly body: string\n ) {\n super(message);\n this.name = 'FlyApiError';\n }\n}\n\nconst BASE_URL = 'https://api.machines.dev/v1';\n\nexport class FlyApiClient {\n private readonly token: string;\n\n constructor(token: string) {\n this.token = token;\n }\n\n private async request<T>(\n method: string,\n path: string,\n body?: unknown\n ): Promise<T> {\n const url = `${BASE_URL}${path}`;\n const headers: Record<string, string> = {\n Authorization: `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n };\n\n const response = await fetch(url, {\n method,\n headers,\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n\n const text = await response.text();\n\n if (!response.ok) {\n throw new FlyApiError(\n `Fly API error ${response.status} for ${method} ${path}: ${text}`,\n response.status,\n text\n );\n }\n\n if (!text) return undefined as T;\n\n try {\n return JSON.parse(text) as T;\n } catch {\n return text as unknown as T;\n }\n }\n\n /** Create a machine in an app */\n async createMachine(\n appName: string,\n name: string,\n config: FlyMachineConfig\n ): Promise<FlyMachine> {\n return this.request<FlyMachine>('POST', `/apps/${appName}/machines`, {\n name,\n config: {\n image: config.image,\n env: config.env,\n guest: config.size\n ? { cpu_kind: 'shared', cpus: 2, memory_mb: config.memory ?? 1024 }\n : undefined,\n restart: config.restart ?? { policy: 'no' },\n auto_destroy: config.auto_destroy,\n metadata: config.metadata,\n },\n region: config.region,\n });\n }\n\n /** Destroy a machine (force=true for immediate) */\n async destroyMachine(appName: string, machineId: string): Promise<void> {\n await this.request<void>(\n 'DELETE',\n `/apps/${appName}/machines/${machineId}?force=true`\n );\n }\n\n /** Start a stopped machine */\n async startMachine(appName: string, machineId: string): Promise<void> {\n await this.request<void>(\n 'POST',\n `/apps/${appName}/machines/${machineId}/start`\n );\n }\n\n /** Stop a running machine */\n async stopMachine(\n appName: string,\n machineId: string,\n signal?: string,\n timeout?: number\n ): Promise<void> {\n await this.request<void>(\n 'POST',\n `/apps/${appName}/machines/${machineId}/stop`,\n signal || timeout ? { signal, timeout } : undefined\n );\n }\n\n /** Get a machine by ID */\n async getMachine(appName: string, machineId: string): Promise<FlyMachine> {\n return this.request<FlyMachine>(\n 'GET',\n `/apps/${appName}/machines/${machineId}`\n );\n }\n\n /** List all machines in an app */\n async listMachines(appName: string): Promise<FlyMachine[]> {\n const result = await this.request<FlyMachine[] | null>(\n 'GET',\n `/apps/${appName}/machines`\n );\n return result ?? [];\n }\n\n /** Execute a command inside a running machine */\n async execCommand(\n appName: string,\n machineId: string,\n command: string[],\n timeout: number = 30\n ): Promise<FlyExecResult> {\n return this.request<FlyExecResult>(\n 'POST',\n `/apps/${appName}/machines/${machineId}/exec`,\n { command, timeout }\n );\n }\n\n /** Wait for a machine to reach a target state */\n async waitForState(\n appName: string,\n machineId: string,\n state: string,\n timeout: number = 60\n ): Promise<void> {\n await this.request<void>(\n 'GET',\n `/apps/${appName}/machines/${machineId}/wait?state=${state}&timeout=${timeout}`\n );\n }\n\n /** Create a Fly app if it doesn't exist */\n async ensureApp(appName: string, orgSlug: string): Promise<void> {\n try {\n await this.request<unknown>('GET', `/apps/${appName}`);\n } catch (err) {\n if (err instanceof FlyApiError && err.statusCode === 404) {\n await this.request<unknown>('POST', '/apps', {\n app_name: appName,\n org_slug: orgSlug,\n network: 'default',\n });\n } else {\n throw err;\n }\n }\n }\n}\n\n/** Create a FlyApiClient from env or explicit token */\nexport function createFlyApiClient(token?: string): FlyApiClient {\n const tok = token ?? process.env.FLY_API_TOKEN;\n if (!tok) {\n throw new Error(\n 'Fly API token not found. Set FLY_API_TOKEN environment variable or run: fly auth login'\n );\n }\n return new FlyApiClient(tok);\n}\n","/**\n * Fly.io Remote Provider\n *\n * Implements the RemoteProvider interface using Fly Machines API and Fly CLI.\n * VM lifecycle is managed via REST API; exec/SSH via Fly CLI.\n */\n\nimport { exec, spawn } from 'child_process';\nimport { promisify } from 'util';\nimport { existsSync, readFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { parse } from 'yaml';\nimport { FlyApiClient, createFlyApiClient, FlyApiError } from './fly-api.js';\nimport type { RemoteProvider, VmInfo, VmStatus, ExecResult } from './interface.js';\n\nconst execAsync = promisify(exec);\n\nexport interface FlyProviderConfig {\n /** Fly.io app name for workspace machines (default: pan-workspaces) */\n app?: string;\n /** Fly.io org slug */\n org?: string;\n /** Default region (default: iad) */\n region?: string;\n /** Machine size (default: shared-cpu-2x) */\n vmSize?: string;\n /** Memory in MB (default: 1024) */\n vmMemory?: number;\n /** Docker image for workspace machines */\n image?: string;\n /** API token (falls back to FLY_API_TOKEN env var) */\n apiToken?: string;\n}\n\nfunction mapFlyStateToVmStatus(state: string): VmStatus {\n switch (state) {\n case 'started':\n return 'running';\n case 'stopped':\n case 'suspended':\n return 'stopped';\n case 'created':\n case 'replacing':\n return 'creating';\n case 'destroying':\n case 'destroyed':\n return 'deleting';\n default:\n return 'unknown';\n }\n}\n\nexport class FlyProvider implements RemoteProvider {\n readonly name = 'fly';\n\n private readonly config: Required<FlyProviderConfig>;\n private api: FlyApiClient | null = null;\n\n constructor(config: FlyProviderConfig = {}) {\n this.config = {\n app: config.app ?? 'pan-workspaces',\n org: config.org ?? 'personal',\n region: config.region ?? 'iad',\n vmSize: config.vmSize ?? 'shared-cpu-2x',\n vmMemory: config.vmMemory ?? 1024,\n image: config.image ?? 'registry.fly.io/pan-workspace:latest',\n apiToken: config.apiToken ?? process.env.FLY_API_TOKEN ?? '',\n };\n }\n\n private getApi(): FlyApiClient {\n if (!this.api) {\n this.api = createFlyApiClient(this.config.apiToken || undefined);\n }\n return this.api;\n }\n\n async isAuthenticated(): Promise<boolean> {\n // Check API token first\n if (this.config.apiToken || process.env.FLY_API_TOKEN) {\n try {\n await this.getApi().listMachines(this.config.app);\n return true;\n } catch {\n // Fall through to CLI check\n }\n }\n\n // Check fly CLI auth\n try {\n const result = await execAsync('fly auth whoami', { timeout: 10000 });\n return !result.stdout.includes('not logged in');\n } catch {\n return false;\n }\n }\n\n /**\n * Resolve vmName to {appName, machineId} by scanning workspace metadata.\n * Falls back to listing machines in the app.\n */\n async resolveVm(vmName: string): Promise<{ appName: string; machineId: string }> {\n const workspacesDir = join(homedir(), '.panopticon', 'workspaces');\n if (existsSync(workspacesDir)) {\n for (const file of readdirSync(workspacesDir)) {\n if (!file.endsWith('.yaml')) continue;\n try {\n const content = readFileSync(join(workspacesDir, file), 'utf-8');\n const metadata = parse(content) as { vmName?: string; machineId?: string; appName?: string };\n if (metadata.vmName === vmName && metadata.machineId && metadata.appName) {\n return { appName: metadata.appName, machineId: metadata.machineId };\n }\n } catch {\n // Skip invalid files\n }\n }\n }\n\n // Fallback: search by machine name via API\n const machines = await this.getApi().listMachines(this.config.app);\n const machine = machines.find(m => m.name === vmName);\n if (!machine) {\n throw new Error(`No Fly machine found for VM name: ${vmName}`);\n }\n return { appName: this.config.app, machineId: machine.id };\n }\n\n async createVm(name: string): Promise<VmInfo> {\n const api = this.getApi();\n\n // Ensure app exists\n await api.ensureApp(this.config.app, this.config.org);\n\n // Create machine\n const machine = await api.createMachine(this.config.app, name, {\n image: this.config.image,\n size: this.config.vmSize,\n memory: this.config.vmMemory,\n region: this.config.region,\n restart: { policy: 'no' },\n auto_destroy: false,\n });\n\n // Wait for machine to start\n try {\n await api.waitForState(this.config.app, machine.id, 'started', 120);\n } catch {\n // Non-fatal: machine may still be starting\n }\n\n return {\n name,\n status: mapFlyStateToVmStatus(machine.state),\n machineId: machine.id,\n ipAddress: machine.private_ip,\n created: machine.created_at ? new Date(machine.created_at) : undefined,\n };\n }\n\n async deleteVm(name: string): Promise<void> {\n const { appName, machineId } = await this.resolveVm(name);\n await this.getApi().destroyMachine(appName, machineId);\n }\n\n async listVms(): Promise<VmInfo[]> {\n const machines = await this.getApi().listMachines(this.config.app);\n return machines.map(m => ({\n name: m.name,\n status: mapFlyStateToVmStatus(m.state),\n machineId: m.id,\n ipAddress: m.private_ip,\n created: m.created_at ? new Date(m.created_at) : undefined,\n }));\n }\n\n async getStatus(name: string): Promise<VmStatus> {\n try {\n const { appName, machineId } = await this.resolveVm(name);\n const machine = await this.getApi().getMachine(appName, machineId);\n return mapFlyStateToVmStatus(machine.state);\n } catch {\n return 'unknown';\n }\n }\n\n async getVmInfo(name: string): Promise<VmInfo | null> {\n try {\n const { appName, machineId } = await this.resolveVm(name);\n const machine = await this.getApi().getMachine(appName, machineId);\n return {\n name,\n status: mapFlyStateToVmStatus(machine.state),\n machineId: machine.id,\n ipAddress: machine.private_ip,\n created: machine.created_at ? new Date(machine.created_at) : undefined,\n memoryTotal: machine.config?.guest?.memory_mb,\n };\n } catch {\n return null;\n }\n }\n\n async startVm(name: string): Promise<void> {\n const { appName, machineId } = await this.resolveVm(name);\n await this.getApi().startMachine(appName, machineId);\n await this.getApi().waitForState(appName, machineId, 'started', 60);\n }\n\n async stopVm(name: string): Promise<void> {\n const { appName, machineId } = await this.resolveVm(name);\n await this.getApi().stopMachine(appName, machineId);\n }\n\n /** Execute a command on the VM via Fly Machines exec API */\n async ssh(vm: string, command: string): Promise<ExecResult> {\n const { appName, machineId } = await this.resolveVm(vm);\n try {\n const result = await this.getApi().execCommand(\n appName,\n machineId,\n ['/bin/sh', '-c', command],\n 60\n );\n return {\n stdout: result.stdout ?? '',\n stderr: result.stderr ?? '',\n exitCode: result.exit_code ?? 0,\n };\n } catch (err) {\n if (err instanceof FlyApiError) {\n return { stdout: '', stderr: err.message, exitCode: 1 };\n }\n throw err;\n }\n }\n\n /** Stream command output via fly SSH console */\n async *sshStream(vm: string, command: string): AsyncIterable<string> {\n const { appName } = await this.resolveVm(vm);\n const child = spawn('fly', ['ssh', 'console', '-a', appName, '-C', command], {\n env: { ...process.env },\n });\n\n for await (const chunk of child.stdout) {\n yield chunk.toString();\n }\n for await (const chunk of child.stderr) {\n yield chunk.toString();\n }\n\n await new Promise<void>((resolve, reject) => {\n child.on('close', resolve);\n child.on('error', reject);\n });\n }\n\n /** Copy a local file to VM using base64 encoding */\n async copyToVm(vm: string, localPath: string, remotePath: string): Promise<void> {\n const content = readFileSync(localPath);\n const b64 = content.toString('base64');\n const dirPath = remotePath.substring(0, remotePath.lastIndexOf('/'));\n if (dirPath) {\n await this.ssh(vm, `mkdir -p ${JSON.stringify(dirPath)}`);\n }\n await this.ssh(vm, `echo '${b64}' | base64 -d > ${JSON.stringify(remotePath)}`);\n }\n\n /** Copy a file from VM to local path */\n async copyFromVm(vm: string, remotePath: string, localPath: string): Promise<void> {\n const { appName } = await this.resolveVm(vm);\n await execAsync(\n `fly ssh sftp get -a ${JSON.stringify(appName)} ${JSON.stringify(remotePath)} ${JSON.stringify(localPath)}`,\n { timeout: 60000 }\n );\n }\n\n /** Expose a port — not supported by Fly.io provider */\n async exposePort(_vm: string, _port: number): Promise<string> {\n throw new Error(\n 'exposePort is not supported by the Fly.io provider. ' +\n 'Configure services in fly.toml or via the Fly Machines API config.'\n );\n }\n\n /** Create a fly proxy tunnel to the machine */\n async tunnel(\n vm: string,\n remotePort: number,\n localPort: number\n ): Promise<{ close: () => void }> {\n const { appName } = await this.resolveVm(vm);\n const child = spawn('fly', ['proxy', `${localPort}:${remotePort}`, '-a', appName], {\n env: { ...process.env },\n });\n\n return {\n close: () => {\n child.kill();\n },\n };\n }\n\n // ============================================================================\n // Credential Sync & Configuration (ported from ExeProvider)\n // ============================================================================\n\n /** Sync Claude Code credentials from local macOS Keychain to remote VM */\n async syncClaudeCredentials(vmName: string): Promise<boolean> {\n try {\n const { stdout: credentials } = await execAsync(\n 'security find-generic-password -s \"Claude Code-credentials\" -w 2>/dev/null',\n { encoding: 'utf-8', timeout: 10000 }\n );\n if (!credentials?.trim()) return false;\n\n const b64 = Buffer.from(credentials.trim()).toString('base64');\n await this.ssh(vmName, `mkdir -p ~/.claude && echo '${b64}' | base64 -d > ~/.claude/.credentials.json`);\n return true;\n } catch {\n return false;\n }\n }\n\n /** Sync GitHub CLI authentication to the remote VM */\n async syncGitHubAuth(vmName: string): Promise<boolean> {\n const ghConfigPath = join(homedir(), '.config', 'gh', 'hosts.yml');\n if (!existsSync(ghConfigPath)) return false;\n\n try {\n const content = readFileSync(ghConfigPath, 'utf-8');\n const b64 = Buffer.from(content).toString('base64');\n await this.ssh(vmName, `mkdir -p ~/.config/gh && echo '${b64}' | base64 -d > ~/.config/gh/hosts.yml`);\n return true;\n } catch {\n return false;\n }\n }\n\n /** Sync GitLab CLI (glab) authentication to the remote VM */\n async syncGitLabAuth(vmName: string): Promise<boolean> {\n const glabConfigPath = join(homedir(), '.config', 'glab-cli', 'config.yml');\n if (!existsSync(glabConfigPath)) return false;\n\n try {\n const content = readFileSync(glabConfigPath, 'utf-8');\n const b64 = Buffer.from(content).toString('base64');\n await this.ssh(vmName, `mkdir -p ~/.config/glab-cli && echo '${b64}' | base64 -d > ~/.config/glab-cli/config.yml`);\n return true;\n } catch {\n return false;\n }\n }\n\n /** Sync all credentials needed for remote workspace operation */\n async syncAllCredentials(vmName: string): Promise<{ claude: boolean; github: boolean }> {\n const [claude, github] = await Promise.all([\n this.syncClaudeCredentials(vmName),\n this.syncGitHubAuth(vmName),\n ]);\n return { claude, github };\n }\n\n /** Install beads CLI (bd) on a remote VM */\n async installBeads(vmName: string): Promise<boolean> {\n // Check if already installed\n const check = await this.ssh(vmName, 'which bd 2>/dev/null');\n if (check.exitCode === 0 && check.stdout.trim()) return true;\n\n // Install via npm\n const result = await this.ssh(vmName, 'npm install -g @beads-dev/beads 2>&1');\n if (result.exitCode !== 0) {\n // Try alternative install\n const alt = await this.ssh(\n vmName,\n 'curl -fsSL https://raw.githubusercontent.com/beads-dev/beads/main/install.sh | bash 2>&1'\n );\n return alt.exitCode === 0;\n }\n return true;\n }\n\n /** Initialize beads in a workspace on a remote VM */\n async initBeads(vmName: string, workspacePath: string = '/workspace'): Promise<boolean> {\n const result = await this.ssh(\n vmName,\n `cd ${workspacePath} && (bd init --prefix PAN 2>&1 || bd init 2>&1) && git config beads.role contributor`\n );\n return result.exitCode === 0;\n }\n\n /** Configure Claude Code on a VM for autonomous operation */\n async configureClaudeCode(vmName: string): Promise<void> {\n await this.ssh(vmName, 'mkdir -p ~/.claude');\n\n // Set onboarding complete\n const onboardingScript = `\nimport json, os\npath = os.path.expanduser(\"~/.claude.json\")\ndata = {}\nif os.path.exists(path):\n with open(path) as f:\n data = json.load(f)\ndata[\"hasCompletedOnboarding\"] = True\ndata[\"lastOnboardingVersion\"] = \"2.0.50\"\nwith open(path, \"w\") as f:\n json.dump(data, f, indent=2)\n`;\n const scriptB64 = Buffer.from(onboardingScript).toString('base64');\n await this.ssh(vmName, `echo '${scriptB64}' | base64 -d | python3`);\n\n // Write settings.json with bypass permissions\n const settings = JSON.stringify({\n theme: 'dark',\n permissions: { defaultMode: 'bypassPermissions' },\n });\n const settingsB64 = Buffer.from(settings).toString('base64');\n await this.ssh(vmName, `echo '${settingsB64}' | base64 -d > ~/.claude/settings.json`);\n }\n\n /** Copy essential skills from local ~/.panopticon/skills/ to remote VM */\n async copySkillsToVm(vmName: string): Promise<void> {\n const skillsDir = join(homedir(), '.panopticon', 'skills');\n if (!existsSync(skillsDir)) return;\n\n await this.ssh(vmName, 'mkdir -p ~/.claude/skills');\n\n try {\n const entries = readdirSync(skillsDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.md')) continue;\n const localPath = join(skillsDir, entry.name);\n const content = readFileSync(localPath, 'utf-8');\n const b64 = Buffer.from(content).toString('base64');\n await this.ssh(vmName, `echo '${b64}' | base64 -d > ~/.claude/skills/${entry.name}`);\n }\n } catch {\n // Non-fatal: skills are optional\n }\n }\n\n /** Sync beads from remote VM to git: exports JSONL, commits, and pushes */\n async syncBeadsToGit(\n vmName: string,\n workspacePath: string = '/workspace',\n commitMessage?: string\n ): Promise<boolean> {\n const msg = commitMessage ?? 'chore: sync beads from remote';\n\n // Export beads to JSONL\n const exportResult = await this.ssh(\n vmName,\n `cd ${workspacePath} && bd export --output .beads/issues.jsonl 2>&1`\n );\n if (exportResult.exitCode !== 0) {\n return false;\n }\n\n // Commit and push\n const gitResult = await this.ssh(\n vmName,\n `cd ${workspacePath} && git diff --cached --quiet || (git commit -m ${JSON.stringify(msg)} && git push origin HEAD) 2>&1`\n );\n return gitResult.exitCode === 0;\n }\n\n /** Query beads on a remote VM via bd search */\n async queryBeads(\n vmName: string,\n searchTerm: string,\n workspacePath: string = '/workspace'\n ): Promise<unknown[]> {\n const result = await this.ssh(\n vmName,\n `cd ${workspacePath} && bd search ${JSON.stringify(searchTerm)} --json 2>/dev/null || echo '[]'`\n );\n try {\n return JSON.parse(result.stdout.trim() || '[]');\n } catch {\n return [];\n }\n }\n\n /** Get the configured app name */\n getAppName(): string {\n return this.config.app;\n }\n}\n\nexport function createFlyProvider(config?: FlyProviderConfig): FlyProvider {\n return new FlyProvider(config);\n}\n","/**\n * Remote Agent Management\n *\n * Spawn and manage Claude agents on remote Fly.io machines.\n * Agents run in tmux sessions for persistence and monitoring.\n */\n\nimport { createFlyProvider } from './fly-provider.js';\nimport type { RemoteProvider, RemoteWorkspaceMetadata } from './interface.js';\nimport { join } from 'path';\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { homedir } from 'os';\n\nconst AGENTS_DIR = join(homedir(), '.panopticon', 'agents');\n\nexport interface RemoteAgentState {\n id: string;\n issueId: string;\n vmName: string;\n model: string;\n status: 'starting' | 'running' | 'stopped' | 'error';\n startedAt: string;\n lastActivity?: string;\n location: 'remote';\n}\n\n/**\n * Get agent state file path\n */\nfunction getRemoteAgentStateFile(agentId: string): string {\n return join(AGENTS_DIR, agentId, 'remote-state.json');\n}\n\n/**\n * Save remote agent state\n */\nfunction saveRemoteAgentState(state: RemoteAgentState): void {\n const dir = join(AGENTS_DIR, state.id);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(getRemoteAgentStateFile(state.id), JSON.stringify(state, null, 2));\n}\n\n/**\n * Load remote agent state\n */\nexport function loadRemoteAgentState(agentId: string): RemoteAgentState | null {\n const file = getRemoteAgentStateFile(agentId);\n if (!existsSync(file)) return null;\n\n try {\n return JSON.parse(readFileSync(file, 'utf-8'));\n } catch {\n return null;\n }\n}\n\n/**\n * Check if remote agent session exists\n */\nasync function remoteSessionExists(\n provider: RemoteProvider,\n vmName: string,\n sessionName: string\n): Promise<boolean> {\n const result = await provider.ssh(vmName, `tmux has-session -t ${sessionName} 2>/dev/null && echo exists || echo not-found`);\n return result.stdout.trim() === 'exists';\n}\n\nexport interface SpawnRemoteAgentOptions {\n issueId: string;\n workspace: RemoteWorkspaceMetadata;\n model?: string;\n prompt?: string;\n phase?: string;\n}\n\n/**\n * Spawn a Claude agent on a remote VM\n */\nexport async function spawnRemoteAgent(options: SpawnRemoteAgentOptions): Promise<RemoteAgentState> {\n const { issueId, workspace, model = 'claude-sonnet-4-6', prompt } = options;\n\n const agentId = `agent-${issueId.toLowerCase()}`;\n const vmName = workspace.vmName;\n\n const fly = createFlyProvider();\n\n // Check if VM is running\n const vmStatus = await fly.getStatus(vmName);\n if (vmStatus !== 'running') {\n throw new Error(`VM ${vmName} is not running. Start it with: pan workspace start ${issueId}`);\n }\n\n // Check if agent already exists\n if (await remoteSessionExists(fly, vmName, agentId)) {\n throw new Error(`Agent ${agentId} already running on ${vmName}. Use 'pan work tell' to message it.`);\n }\n\n // Create agent state\n const state: RemoteAgentState = {\n id: agentId,\n issueId,\n vmName,\n model,\n status: 'starting',\n startedAt: new Date().toISOString(),\n location: 'remote',\n };\n\n saveRemoteAgentState(state);\n\n // Write prompt to file on remote VM if provided\n let claudeCmd: string;\n\n if (prompt) {\n // Write prompt to file on VM using base64 to avoid escaping issues\n const promptFile = `/workspace/.pan/prompts/${agentId}.md`;\n await fly.ssh(vmName, `mkdir -p /workspace/.pan/prompts`);\n const promptBase64 = Buffer.from(prompt).toString('base64');\n await fly.ssh(vmName, `echo '${promptBase64}' | base64 -d > ${promptFile}`);\n\n // Create launcher script using base64 to avoid shell interpretation\n const launcherScript = `/workspace/.pan/prompts/${agentId}-launcher.sh`;\n const launcherContent = `#!/bin/bash\nexport PATH=\"/usr/local/bin:\\$PATH\"\nprompt=\\$(cat \"${promptFile}\")\nexec claude --dangerously-skip-permissions --model ${model} \"\\$prompt\"\n`;\n const launcherBase64 = Buffer.from(launcherContent).toString('base64');\n await fly.ssh(vmName, `echo '${launcherBase64}' | base64 -d > ${launcherScript} && chmod +x ${launcherScript}`);\n\n claudeCmd = `bash ${launcherScript}`;\n } else {\n claudeCmd = `claude --dangerously-skip-permissions --model ${model}`;\n }\n\n // Create tmux session on remote VM\n const tmuxCmd = `tmux new-session -d -s ${agentId} -c /workspace '${claudeCmd}'`;\n const result = await fly.ssh(vmName, tmuxCmd);\n\n if (result.exitCode !== 0) {\n state.status = 'error';\n saveRemoteAgentState(state);\n throw new Error(`Failed to start agent: ${result.stderr}`);\n }\n\n // Update status\n state.status = 'running';\n saveRemoteAgentState(state);\n\n return state;\n}\n\n/**\n * Get remote agent output from tmux session\n */\nexport async function getRemoteAgentOutput(\n agentId: string,\n vmName: string,\n lines: number = 100\n): Promise<string> {\n const fly = createFlyProvider();\n\n const result = await fly.ssh(vmName, `tmux capture-pane -t ${agentId} -p -S -${lines}`);\n return result.stdout;\n}\n\n/**\n * Send message to remote agent\n */\nexport async function sendToRemoteAgent(\n agentId: string,\n vmName: string,\n message: string\n): Promise<void> {\n const fly = createFlyProvider();\n\n // Escape message for shell\n const escapedMessage = message.replace(/'/g, \"'\\\\''\");\n\n // Send keys to tmux session (send message then Enter)\n await fly.ssh(vmName, `tmux send-keys -t ${agentId} '${escapedMessage}'`);\n await fly.ssh(vmName, `tmux send-keys -t ${agentId} C-m`);\n}\n\n/**\n * Check if remote agent is still running\n */\nexport async function isRemoteAgentRunning(\n agentId: string,\n vmName: string\n): Promise<boolean> {\n const fly = createFlyProvider();\n return remoteSessionExists(fly, vmName, agentId);\n}\n\n/**\n * Kill remote agent session\n */\nexport async function killRemoteAgent(\n agentId: string,\n vmName: string\n): Promise<void> {\n const fly = createFlyProvider();\n await fly.ssh(vmName, `tmux kill-session -t ${agentId} 2>/dev/null || true`);\n\n // Update state\n const state = loadRemoteAgentState(agentId);\n if (state) {\n state.status = 'stopped';\n saveRemoteAgentState(state);\n }\n}\n\n/**\n * Get list of running remote agents on a VM\n */\nexport async function listRemoteAgents(vmName: string): Promise<string[]> {\n const fly = createFlyProvider();\n\n const result = await fly.ssh(vmName, `tmux list-sessions -F \"#{session_name}\" 2>/dev/null | grep \"^agent-\" || true`);\n if (!result.stdout.trim()) {\n return [];\n }\n\n return result.stdout.trim().split('\\n').filter(Boolean);\n}\n\n/**\n * Poll remote agent for status updates\n * Returns parsed events from the agent output\n */\nexport async function pollRemoteAgentStatus(\n agentId: string,\n vmName: string\n): Promise<{\n isRunning: boolean;\n lastOutput: string;\n toolUses: string[];\n}> {\n const fly = createFlyProvider();\n\n // Check if session exists\n const isRunning = await remoteSessionExists(fly, vmName, agentId);\n\n if (!isRunning) {\n return { isRunning: false, lastOutput: '', toolUses: [] };\n }\n\n // Get recent output\n const output = await getRemoteAgentOutput(agentId, vmName, 50);\n\n // Parse tool uses from output (simple pattern matching)\n const toolUses: string[] = [];\n const toolPattern = /(?:Using|Calling|Running)\\s+(\\w+)\\s+tool/gi;\n let match;\n while ((match = toolPattern.exec(output)) !== null) {\n toolUses.push(match[1]);\n }\n\n return {\n isRunning,\n lastOutput: output,\n toolUses,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as loadReviewStatuses, i as init_review_status, n as clearStuckMergeStatuses, o as saveReviewStatuses, r as getReviewStatus, s as setReviewStatus, t as clearReviewStatus } from "./review-status-
|
|
1
|
+
import { a as loadReviewStatuses, i as init_review_status, n as clearStuckMergeStatuses, o as saveReviewStatuses, r as getReviewStatus, s as setReviewStatus, t as clearReviewStatus } from "./review-status-Bymwzh2i.js";
|
|
2
2
|
init_review_status();
|
|
3
3
|
export { clearReviewStatus, clearStuckMergeStatuses, getReviewStatus, loadReviewStatuses, saveReviewStatuses, setReviewStatus };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as __esmMin } from "./chunk-DORXReHP.js";
|
|
2
|
-
import { n as notifyPipeline, t as init_pipeline_notifier } from "./pipeline-notifier-
|
|
3
|
-
import { i as init_database, n as getDatabase } from "./database-
|
|
2
|
+
import { n as notifyPipeline, t as init_pipeline_notifier } from "./pipeline-notifier-CCSN-jar.js";
|
|
3
|
+
import { i as init_database, n as getDatabase } from "./database-cxmQryoh.js";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
5
|
import { dirname, join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
@@ -189,7 +189,7 @@ function setReviewStatus(issueId, update, filePath = DEFAULT_STATUS_FILE) {
|
|
|
189
189
|
};
|
|
190
190
|
if (readyForMerge && !existing.readyForMerge && updated.prUrl) (async () => {
|
|
191
191
|
try {
|
|
192
|
-
const { isGitHubAppConfigured, reportCommitStatus } = await import("./github-app-
|
|
192
|
+
const { isGitHubAppConfigured, reportCommitStatus } = await import("./github-app-DClWjjHr.js");
|
|
193
193
|
if (!isGitHubAppConfigured()) return;
|
|
194
194
|
const prMatch = updated.prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull/);
|
|
195
195
|
if (!prMatch) return;
|
|
@@ -222,6 +222,46 @@ function setReviewStatus(issueId, update, filePath = DEFAULT_STATUS_FILE) {
|
|
|
222
222
|
issueId,
|
|
223
223
|
status: updated
|
|
224
224
|
});
|
|
225
|
+
if (update.reviewStatus === "passed" && existing.reviewStatus !== "passed" && existing.testStatus === "pending") (async () => {
|
|
226
|
+
try {
|
|
227
|
+
const { submitToSpecialistQueue } = await import("./specialists-Cp-PgspS.js");
|
|
228
|
+
const workAgentId = `agent-${issueId.toLowerCase()}`;
|
|
229
|
+
const workStateFile = join(homedir(), ".panopticon", "agents", workAgentId, "state.json");
|
|
230
|
+
let workspace;
|
|
231
|
+
let branch;
|
|
232
|
+
if (existsSync(workStateFile)) try {
|
|
233
|
+
const workState = JSON.parse(readFileSync(workStateFile, "utf-8"));
|
|
234
|
+
workspace = workState.workspace;
|
|
235
|
+
branch = workState.branch || `feature/${issueId.toLowerCase()}`;
|
|
236
|
+
} catch {}
|
|
237
|
+
submitToSpecialistQueue("test-agent", {
|
|
238
|
+
priority: "high",
|
|
239
|
+
source: "review-agent-auto",
|
|
240
|
+
issueId,
|
|
241
|
+
workspace,
|
|
242
|
+
branch
|
|
243
|
+
});
|
|
244
|
+
console.log(`[review-status] Queued test-agent for ${issueId} after review passed`);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.warn(`[review-status] Failed to queue test-agent for ${issueId}: ${err.message}`);
|
|
247
|
+
}
|
|
248
|
+
})();
|
|
249
|
+
if ((update.reviewStatus === "blocked" || update.testStatus === "failed") && (update.reviewStatus !== existing.reviewStatus || update.testStatus !== existing.testStatus)) {
|
|
250
|
+
const agentSession = `agent-${issueId.toLowerCase()}`;
|
|
251
|
+
(async () => {
|
|
252
|
+
try {
|
|
253
|
+
const { sessionExists } = await import("./tmux-BzxdKItf.js");
|
|
254
|
+
if (!sessionExists(agentSession)) return;
|
|
255
|
+
const statusType = update.reviewStatus === "blocked" ? "REVIEW BLOCKED" : "TESTS FAILED";
|
|
256
|
+
const msg = `SPECIALIST FEEDBACK: ${statusType} for ${issueId}.\n\n${update.reviewNotes || update.testNotes || "No details provided."}\n\nFix the issues, then run: pan work done ${issueId}`;
|
|
257
|
+
const { messageAgent } = await import("./agents-Dgh2TjSp.js");
|
|
258
|
+
await messageAgent(agentSession, msg);
|
|
259
|
+
console.log(`[review-status] Auto-delivered ${statusType} feedback to ${agentSession}`);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.warn(`[review-status] Failed to auto-deliver feedback to ${agentSession}: ${err.message}`);
|
|
262
|
+
}
|
|
263
|
+
})();
|
|
264
|
+
}
|
|
225
265
|
return updated;
|
|
226
266
|
}
|
|
227
267
|
function getReviewStatus(issueId, filePath = DEFAULT_STATUS_FILE) {
|
|
@@ -262,4 +302,4 @@ var init_review_status = __esmMin((() => {
|
|
|
262
302
|
//#endregion
|
|
263
303
|
export { loadReviewStatuses as a, init_review_status as i, clearStuckMergeStatuses as n, saveReviewStatuses as o, getReviewStatus as r, setReviewStatus as s, clearReviewStatus as t };
|
|
264
304
|
|
|
265
|
-
//# sourceMappingURL=review-status-
|
|
305
|
+
//# sourceMappingURL=review-status-Bymwzh2i.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-status-CV55Tl-n.js","names":[],"sources":["../../src/lib/database/review-status-db.ts","../../src/lib/review-status.ts"],"sourcesContent":["/**\n * Review Status SQLite Storage\n *\n * Provides SQLite-backed CRUD for ReviewStatus, matching the interface in\n * src/lib/review-status.ts. Atomic single-transaction writes eliminate the\n * TOCTOU race in the JSON-backed implementation.\n */\n\nimport { getDatabase } from './index.js';\nimport type { ReviewStatus, StatusHistoryEntry } from '../review-status.js';\n\n// ============== Write operations ==============\n\n/**\n * Upsert a review status record atomically.\n * Replaces the JSON read-modify-write cycle with a single transaction.\n */\nexport function upsertReviewStatus(status: ReviewStatus): void {\n const db = getDatabase();\n\n const upsert = db.transaction((s: ReviewStatus) => {\n // Upsert main record\n db.prepare(`\n INSERT INTO review_status (\n issue_id, review_status, test_status, merge_status,\n verification_status, verification_notes,\n verification_cycle_count, verification_max_cycles,\n review_notes, test_notes, merge_notes,\n updated_at, ready_for_merge, auto_requeue_count, pr_url\n ) VALUES (\n ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?\n )\n ON CONFLICT(issue_id) DO UPDATE SET\n review_status = excluded.review_status,\n test_status = excluded.test_status,\n merge_status = excluded.merge_status,\n verification_status = excluded.verification_status,\n verification_notes = excluded.verification_notes,\n verification_cycle_count = excluded.verification_cycle_count,\n verification_max_cycles = excluded.verification_max_cycles,\n review_notes = excluded.review_notes,\n test_notes = excluded.test_notes,\n merge_notes = excluded.merge_notes,\n updated_at = excluded.updated_at,\n ready_for_merge = excluded.ready_for_merge,\n auto_requeue_count = excluded.auto_requeue_count,\n pr_url = excluded.pr_url\n `).run(\n s.issueId,\n s.reviewStatus,\n s.testStatus,\n s.mergeStatus ?? null,\n s.verificationStatus ?? null,\n s.verificationNotes ?? null,\n s.verificationCycleCount ?? null,\n s.verificationMaxCycles ?? null,\n s.reviewNotes ?? null,\n s.testNotes ?? null,\n s.mergeNotes ?? null,\n s.updatedAt,\n s.readyForMerge ? 1 : 0,\n s.autoRequeueCount ?? null,\n s.prUrl ?? null,\n );\n\n // Append new history entries (deduplicate by timestamp to avoid re-inserting)\n if (s.history && s.history.length > 0) {\n const insertHistory = db.prepare(`\n INSERT OR IGNORE INTO status_history (issue_id, type, status, timestamp, notes)\n VALUES (?, ?, ?, ?, ?)\n `);\n for (const entry of s.history) {\n insertHistory.run(s.issueId, entry.type, entry.status, entry.timestamp, entry.notes ?? null);\n }\n }\n });\n\n upsert(status);\n}\n\n/**\n * Delete a review status record and its history.\n */\nexport function deleteReviewStatus(issueId: string): void {\n const db = getDatabase();\n db.prepare('DELETE FROM review_status WHERE issue_id = ?').run(issueId);\n}\n\n// ============== Read operations ==============\n\n/**\n * Get a single review status by issue ID.\n */\nexport function getReviewStatusFromDb(issueId: string): ReviewStatus | null {\n const db = getDatabase();\n\n const row = db.prepare(`\n SELECT * FROM review_status WHERE issue_id = ?\n `).get(issueId) as DbReviewStatusRow | undefined;\n\n if (!row) return null;\n\n const history = getHistoryFromDb(issueId);\n return rowToReviewStatus(row, history);\n}\n\n/**\n * Get all review statuses.\n */\nexport function getAllReviewStatusesFromDb(): Record<string, ReviewStatus> {\n const db = getDatabase();\n\n const rows = db.prepare('SELECT * FROM review_status ORDER BY updated_at DESC').all() as DbReviewStatusRow[];\n const result: Record<string, ReviewStatus> = {};\n\n for (const row of rows) {\n const history = getHistoryFromDb(row.issue_id);\n result[row.issue_id] = rowToReviewStatus(row, history);\n }\n\n return result;\n}\n\n/**\n * Get history entries for an issue.\n */\nfunction getHistoryFromDb(issueId: string): StatusHistoryEntry[] {\n const db = getDatabase();\n const rows = db.prepare(`\n SELECT type, status, timestamp, notes\n FROM status_history\n WHERE issue_id = ?\n ORDER BY timestamp ASC\n `).all(issueId) as Array<{ type: string; status: string; timestamp: string; notes: string | null }>;\n\n return rows.map(r => ({\n type: r.type as 'review' | 'test' | 'merge',\n status: r.status,\n timestamp: r.timestamp,\n ...(r.notes ? { notes: r.notes } : {}),\n }));\n}\n\n// ============== Row mapping ==============\n\ninterface DbReviewStatusRow {\n issue_id: string;\n review_status: string;\n test_status: string;\n merge_status: string | null;\n verification_status: string | null;\n verification_notes: string | null;\n verification_cycle_count: number | null;\n verification_max_cycles: number | null;\n review_notes: string | null;\n test_notes: string | null;\n merge_notes: string | null;\n updated_at: string;\n ready_for_merge: number;\n auto_requeue_count: number | null;\n pr_url: string | null;\n}\n\nfunction rowToReviewStatus(row: DbReviewStatusRow, history: StatusHistoryEntry[]): ReviewStatus {\n return {\n issueId: row.issue_id,\n reviewStatus: row.review_status as ReviewStatus['reviewStatus'],\n testStatus: row.test_status as ReviewStatus['testStatus'],\n mergeStatus: row.merge_status as ReviewStatus['mergeStatus'] ?? undefined,\n verificationStatus: row.verification_status as ReviewStatus['verificationStatus'] ?? undefined,\n verificationNotes: row.verification_notes ?? undefined,\n verificationCycleCount: row.verification_cycle_count ?? undefined,\n verificationMaxCycles: row.verification_max_cycles ?? undefined,\n reviewNotes: row.review_notes ?? undefined,\n testNotes: row.test_notes ?? undefined,\n mergeNotes: row.merge_notes ?? undefined,\n updatedAt: row.updated_at,\n readyForMerge: row.ready_for_merge === 1,\n autoRequeueCount: row.auto_requeue_count ?? undefined,\n prUrl: row.pr_url ?? undefined,\n history: history.length > 0 ? history : undefined,\n };\n}\n","import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { homedir } from 'os';\nimport { notifyPipeline } from './pipeline-notifier.js';\nimport {\n upsertReviewStatus as dbUpsert,\n deleteReviewStatus as dbDelete,\n getReviewStatusFromDb,\n getAllReviewStatusesFromDb,\n} from './database/review-status-db.js';\n\nexport interface StatusHistoryEntry {\n type: 'review' | 'test' | 'merge' | 'inspect' | 'uat';\n status: string;\n timestamp: string;\n notes?: string;\n}\n\nexport interface ReviewStatus {\n issueId: string;\n reviewStatus: 'pending' | 'reviewing' | 'passed' | 'failed' | 'blocked';\n testStatus: 'pending' | 'testing' | 'passed' | 'failed' | 'skipped' | 'dispatch_failed';\n mergeStatus?: 'pending' | 'merging' | 'merged' | 'failed';\n inspectStatus?: 'pending' | 'inspecting' | 'passed' | 'failed';\n inspectNotes?: string;\n uatStatus?: 'pending' | 'testing' | 'passed' | 'failed';\n uatNotes?: string;\n verificationStatus?: 'pending' | 'running' | 'passed' | 'failed' | 'skipped';\n verificationNotes?: string;\n verificationCycleCount?: number;\n verificationMaxCycles?: number;\n reviewNotes?: string;\n testNotes?: string;\n mergeNotes?: string;\n updatedAt: string;\n readyForMerge: boolean;\n autoRequeueCount?: number;\n prUrl?: string;\n history?: StatusHistoryEntry[];\n /** HEAD commit SHA at the time review passed — used to detect new commits after review */\n reviewedAtCommit?: string;\n}\n\nconst DEFAULT_STATUS_FILE = join(homedir(), '.panopticon', 'review-status.json');\n\nexport function loadReviewStatuses(filePath = DEFAULT_STATUS_FILE): Record<string, ReviewStatus> {\n // Prefer SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n return getAllReviewStatusesFromDb();\n } catch {\n // Fall through to JSON on DB error\n }\n }\n\n try {\n if (existsSync(filePath)) {\n return JSON.parse(readFileSync(filePath, 'utf-8'));\n }\n } catch (err) {\n console.error('Failed to load review statuses:', err);\n }\n return {};\n}\n\nexport function saveReviewStatuses(statuses: Record<string, ReviewStatus>, filePath = DEFAULT_STATUS_FILE): void {\n try {\n const dir = dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(filePath, JSON.stringify(statuses, null, 2));\n } catch (err) {\n console.error('Failed to save review statuses:', err);\n }\n}\n\nexport function setReviewStatus(\n issueId: string,\n update: Partial<ReviewStatus>,\n filePath = DEFAULT_STATUS_FILE,\n): ReviewStatus {\n const statuses = loadReviewStatuses(filePath);\n const existing = statuses[issueId] || {\n issueId,\n reviewStatus: 'pending' as const,\n testStatus: 'pending' as const,\n updatedAt: new Date().toISOString(),\n readyForMerge: false,\n };\n\n // Guard: reject reviewStatus regression from 'passed' to 'reviewing' unless the caller\n // is explicitly resetting the merge lifecycle (update includes mergeStatus).\n // This is belt-and-suspenders — endpoint-level guards should catch this first.\n if (update.reviewStatus === 'reviewing' && existing.reviewStatus === 'passed' && update.mergeStatus === undefined) {\n console.warn(`[review-status] Rejecting reviewStatus regression from 'passed' to 'reviewing' for ${issueId} (mergeStatus not being reset)`);\n return existing as ReviewStatus;\n }\n\n const merged = { ...existing, ...update };\n\n // Track status transitions in history (last 10 entries)\n const history = [...(existing.history || [])];\n const now = new Date().toISOString();\n if (update.reviewStatus && update.reviewStatus !== existing.reviewStatus) {\n history.push({ type: 'review', status: update.reviewStatus, timestamp: now, notes: update.reviewNotes });\n }\n if (update.testStatus && update.testStatus !== existing.testStatus) {\n history.push({ type: 'test', status: update.testStatus, timestamp: now, notes: update.testNotes });\n }\n if (update.uatStatus && update.uatStatus !== existing.uatStatus) {\n history.push({ type: 'uat', status: update.uatStatus, timestamp: now, notes: update.uatNotes });\n }\n if (update.mergeStatus && update.mergeStatus !== existing.mergeStatus) {\n history.push({ type: 'merge', status: update.mergeStatus, timestamp: now });\n }\n while (history.length > 10) history.shift();\n\n // readyForMerge is true when all required gates pass.\n // If uatStatus exists (UAT specialist has been involved), it must also be 'passed'.\n // verificationStatus must not be 'failed' — verification catches pre-existing test breakage\n // that scoped test runs (e2e/dashboard) may miss.\n const readyForMerge = update.readyForMerge !== undefined\n ? update.readyForMerge\n : (\n merged.reviewStatus === 'passed' &&\n merged.testStatus === 'passed' &&\n merged.verificationStatus !== 'failed' &&\n merged.mergeStatus !== 'merged' &&\n // If UAT has been initiated, it must pass too\n (merged.uatStatus === undefined || merged.uatStatus === 'passed')\n );\n\n const updated: ReviewStatus = {\n ...merged,\n issueId,\n updatedAt: now,\n readyForMerge,\n history,\n };\n\n // Report commit statuses to GitHub when readyForMerge transitions to true (PAN-536)\n if (readyForMerge && !existing.readyForMerge && updated.prUrl) {\n (async () => {\n try {\n const { isGitHubAppConfigured, reportCommitStatus } = await import('./github-app.js');\n if (!isGitHubAppConfigured()) return;\n const prMatch = updated.prUrl!.match(/github\\.com\\/([^/]+)\\/([^/]+)\\/pull/);\n if (!prMatch) return;\n const [, owner, repo] = prMatch;\n // Get HEAD SHA of the PR branch\n const { exec } = await import('child_process');\n const { promisify } = await import('util');\n const execAsync = promisify(exec);\n const { stdout } = await execAsync(\n `gh pr view ${updated.prUrl!.match(/\\/pull\\/(\\d+)/)?.[1]} --json headRefOid --jq .headRefOid`,\n { encoding: 'utf-8', timeout: 10000 }\n );\n const sha = stdout.trim();\n if (sha) {\n await reportCommitStatus(owner, repo, sha, 'success', 'panopticon/review', 'Review passed');\n await reportCommitStatus(owner, repo, sha, 'success', 'panopticon/test', 'Tests passed');\n console.log(`[review-status] Reported commit statuses for ${issueId} (${sha.slice(0, 8)})`);\n }\n } catch (err: any) {\n console.warn(`[review-status] Failed to report commit status: ${err.message}`);\n }\n })();\n }\n\n // SQLite first — it is the authoritative store (reads prefer SQLite)\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n dbUpsert(updated);\n } catch (err) {\n console.error('[review-status] SQLite write failed (continuing with JSON):', err);\n }\n }\n\n // JSON second — legacy fallback for tools that read review-status.json directly\n statuses[issueId] = updated;\n saveReviewStatuses(statuses, filePath);\n\n notifyPipeline({ type: 'status_changed', issueId, status: updated });\n\n return updated;\n}\n\nexport function getReviewStatus(issueId: string, filePath = DEFAULT_STATUS_FILE): ReviewStatus | null {\n // Prefer SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n const fromDb = getReviewStatusFromDb(issueId);\n if (fromDb) return fromDb;\n } catch {\n // Fall through to JSON on DB error\n }\n }\n const statuses = loadReviewStatuses(filePath);\n return statuses[issueId] || null;\n}\n\n/**\n * On server startup, clear any mergeStatus stuck at 'merging'.\n * Pending merge operations are in-memory only — they don't survive a restart.\n * Any 'merging' status after boot is definitionally stuck (PAN-490).\n */\nexport function clearStuckMergeStatuses(): void {\n const statuses = loadReviewStatuses();\n const stuck = Object.values(statuses).filter(s => s.mergeStatus === 'merging');\n if (stuck.length === 0) return;\n console.log(`[review-status] Clearing ${stuck.length} stuck 'merging' status(es) on startup`);\n for (const s of stuck) {\n setReviewStatus(s.issueId, { mergeStatus: 'pending' });\n }\n}\n\nexport function clearReviewStatus(issueId: string, filePath = DEFAULT_STATUS_FILE): void {\n const statuses = loadReviewStatuses(filePath);\n delete statuses[issueId];\n saveReviewStatuses(statuses, filePath);\n\n // Dual-delete from SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n dbDelete(issueId);\n } catch (err) {\n console.error('[review-status] SQLite delete failed (continuing with JSON):', err);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAiBA,SAAgB,mBAAmB,QAA4B;CAC7D,MAAM,KAAK,aAAa;AAET,IAAG,aAAa,MAAoB;AAEjD,KAAG,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;MAyBT,CAAC,IACD,EAAE,SACF,EAAE,cACF,EAAE,YACF,EAAE,eAAe,MACjB,EAAE,sBAAsB,MACxB,EAAE,qBAAqB,MACvB,EAAE,0BAA0B,MAC5B,EAAE,yBAAyB,MAC3B,EAAE,eAAe,MACjB,EAAE,aAAa,MACf,EAAE,cAAc,MAChB,EAAE,WACF,EAAE,gBAAgB,IAAI,GACtB,EAAE,oBAAoB,MACtB,EAAE,SAAS,KACZ;AAGD,MAAI,EAAE,WAAW,EAAE,QAAQ,SAAS,GAAG;GACrC,MAAM,gBAAgB,GAAG,QAAQ;;;QAG/B;AACF,QAAK,MAAM,SAAS,EAAE,QACpB,eAAc,IAAI,EAAE,SAAS,MAAM,MAAM,MAAM,QAAQ,MAAM,WAAW,MAAM,SAAS,KAAK;;GAGhG,CAEK,OAAO;;;;;AAMhB,SAAgB,mBAAmB,SAAuB;AAC7C,cAAa,CACrB,QAAQ,+CAA+C,CAAC,IAAI,QAAQ;;;;;AAQzE,SAAgB,sBAAsB,SAAsC;CAG1E,MAAM,MAFK,aAAa,CAET,QAAQ;;IAErB,CAAC,IAAI,QAAQ;AAEf,KAAI,CAAC,IAAK,QAAO;AAGjB,QAAO,kBAAkB,KADT,iBAAiB,QAAQ,CACH;;;;;AAMxC,SAAgB,6BAA2D;CAGzE,MAAM,OAFK,aAAa,CAER,QAAQ,uDAAuD,CAAC,KAAK;CACrF,MAAM,SAAuC,EAAE;AAE/C,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,UAAU,iBAAiB,IAAI,SAAS;AAC9C,SAAO,IAAI,YAAY,kBAAkB,KAAK,QAAQ;;AAGxD,QAAO;;;;;AAMT,SAAS,iBAAiB,SAAuC;AAS/D,QARW,aAAa,CACR,QAAQ;;;;;IAKtB,CAAC,IAAI,QAAQ,CAEH,KAAI,OAAM;EACpB,MAAM,EAAE;EACR,QAAQ,EAAE;EACV,WAAW,EAAE;EACb,GAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE;EACtC,EAAE;;AAuBL,SAAS,kBAAkB,KAAwB,SAA6C;AAC9F,QAAO;EACL,SAAS,IAAI;EACb,cAAc,IAAI;EAClB,YAAY,IAAI;EAChB,aAAa,IAAI,gBAA+C,KAAA;EAChE,oBAAoB,IAAI,uBAA6D,KAAA;EACrF,mBAAmB,IAAI,sBAAsB,KAAA;EAC7C,wBAAwB,IAAI,4BAA4B,KAAA;EACxD,uBAAuB,IAAI,2BAA2B,KAAA;EACtD,aAAa,IAAI,gBAAgB,KAAA;EACjC,WAAW,IAAI,cAAc,KAAA;EAC7B,YAAY,IAAI,eAAe,KAAA;EAC/B,WAAW,IAAI;EACf,eAAe,IAAI,oBAAoB;EACvC,kBAAkB,IAAI,sBAAsB,KAAA;EAC5C,OAAO,IAAI,UAAU,KAAA;EACrB,SAAS,QAAQ,SAAS,IAAI,UAAU,KAAA;EACzC;;;gBA7KsC;;;;ACqCzC,SAAgB,mBAAmB,WAAW,qBAAmD;AAE/F,KAAI,aAAa,oBACf,KAAI;AACF,SAAO,4BAA4B;SAC7B;AAKV,KAAI;AACF,MAAI,WAAW,SAAS,CACtB,QAAO,KAAK,MAAM,aAAa,UAAU,QAAQ,CAAC;UAE7C,KAAK;AACZ,UAAQ,MAAM,mCAAmC,IAAI;;AAEvD,QAAO,EAAE;;AAGX,SAAgB,mBAAmB,UAAwC,WAAW,qBAA2B;AAC/G,KAAI;EACF,MAAM,MAAM,QAAQ,SAAS;AAC7B,MAAI,CAAC,WAAW,IAAI,CAClB,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;AAErC,gBAAc,UAAU,KAAK,UAAU,UAAU,MAAM,EAAE,CAAC;UACnD,KAAK;AACZ,UAAQ,MAAM,mCAAmC,IAAI;;;AAIzD,SAAgB,gBACd,SACA,QACA,WAAW,qBACG;CACd,MAAM,WAAW,mBAAmB,SAAS;CAC7C,MAAM,WAAW,SAAS,YAAY;EACpC;EACA,cAAc;EACd,YAAY;EACZ,4BAAW,IAAI,MAAM,EAAC,aAAa;EACnC,eAAe;EAChB;AAKD,KAAI,OAAO,iBAAiB,eAAe,SAAS,iBAAiB,YAAY,OAAO,gBAAgB,KAAA,GAAW;AACjH,UAAQ,KAAK,sFAAsF,QAAQ,gCAAgC;AAC3I,SAAO;;CAGT,MAAM,SAAS;EAAE,GAAG;EAAU,GAAG;EAAQ;CAGzC,MAAM,UAAU,CAAC,GAAI,SAAS,WAAW,EAAE,CAAE;CAC7C,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AACpC,KAAI,OAAO,gBAAgB,OAAO,iBAAiB,SAAS,aAC1D,SAAQ,KAAK;EAAE,MAAM;EAAU,QAAQ,OAAO;EAAc,WAAW;EAAK,OAAO,OAAO;EAAa,CAAC;AAE1G,KAAI,OAAO,cAAc,OAAO,eAAe,SAAS,WACtD,SAAQ,KAAK;EAAE,MAAM;EAAQ,QAAQ,OAAO;EAAY,WAAW;EAAK,OAAO,OAAO;EAAW,CAAC;AAEpG,KAAI,OAAO,aAAa,OAAO,cAAc,SAAS,UACpD,SAAQ,KAAK;EAAE,MAAM;EAAO,QAAQ,OAAO;EAAW,WAAW;EAAK,OAAO,OAAO;EAAU,CAAC;AAEjG,KAAI,OAAO,eAAe,OAAO,gBAAgB,SAAS,YACxD,SAAQ,KAAK;EAAE,MAAM;EAAS,QAAQ,OAAO;EAAa,WAAW;EAAK,CAAC;AAE7E,QAAO,QAAQ,SAAS,GAAI,SAAQ,OAAO;CAM3C,MAAM,gBAAgB,OAAO,kBAAkB,KAAA,IAC3C,OAAO,gBAEL,OAAO,iBAAiB,YACxB,OAAO,eAAe,YACtB,OAAO,uBAAuB,YAC9B,OAAO,gBAAgB,aAEtB,OAAO,cAAc,KAAA,KAAa,OAAO,cAAc;CAG9D,MAAM,UAAwB;EAC5B,GAAG;EACH;EACA,WAAW;EACX;EACA;EACD;AAGD,KAAI,iBAAiB,CAAC,SAAS,iBAAiB,QAAQ,MACtD,EAAC,YAAY;AACX,MAAI;GACF,MAAM,EAAE,uBAAuB,uBAAuB,MAAM,OAAO;AACnE,OAAI,CAAC,uBAAuB,CAAE;GAC9B,MAAM,UAAU,QAAQ,MAAO,MAAM,sCAAsC;AAC3E,OAAI,CAAC,QAAS;GACd,MAAM,GAAG,OAAO,QAAQ;GAExB,MAAM,EAAE,SAAS,MAAM,OAAO;GAC9B,MAAM,EAAE,cAAc,MAAM,OAAO;GAEnC,MAAM,EAAE,WAAW,MADD,UAAU,KAAK,CAE/B,cAAc,QAAQ,MAAO,MAAM,gBAAgB,GAAG,GAAG,sCACzD;IAAE,UAAU;IAAS,SAAS;IAAO,CACtC;GACD,MAAM,MAAM,OAAO,MAAM;AACzB,OAAI,KAAK;AACP,UAAM,mBAAmB,OAAO,MAAM,KAAK,WAAW,qBAAqB,gBAAgB;AAC3F,UAAM,mBAAmB,OAAO,MAAM,KAAK,WAAW,mBAAmB,eAAe;AACxF,YAAQ,IAAI,gDAAgD,QAAQ,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG;;WAEtF,KAAU;AACjB,WAAQ,KAAK,mDAAmD,IAAI,UAAU;;KAE9E;AAIN,KAAI,aAAa,oBACf,KAAI;AACF,qBAAS,QAAQ;UACV,KAAK;AACZ,UAAQ,MAAM,+DAA+D,IAAI;;AAKrF,UAAS,WAAW;AACpB,oBAAmB,UAAU,SAAS;AAEtC,gBAAe;EAAE,MAAM;EAAkB;EAAS,QAAQ;EAAS,CAAC;AAEpE,QAAO;;AAGT,SAAgB,gBAAgB,SAAiB,WAAW,qBAA0C;AAEpG,KAAI,aAAa,oBACf,KAAI;EACF,MAAM,SAAS,sBAAsB,QAAQ;AAC7C,MAAI,OAAQ,QAAO;SACb;AAKV,QADiB,mBAAmB,SAAS,CAC7B,YAAY;;;;;;;AAQ9B,SAAgB,0BAAgC;CAC9C,MAAM,WAAW,oBAAoB;CACrC,MAAM,QAAQ,OAAO,OAAO,SAAS,CAAC,QAAO,MAAK,EAAE,gBAAgB,UAAU;AAC9E,KAAI,MAAM,WAAW,EAAG;AACxB,SAAQ,IAAI,4BAA4B,MAAM,OAAO,wCAAwC;AAC7F,MAAK,MAAM,KAAK,MACd,iBAAgB,EAAE,SAAS,EAAE,aAAa,WAAW,CAAC;;AAI1D,SAAgB,kBAAkB,SAAiB,WAAW,qBAA2B;CACvF,MAAM,WAAW,mBAAmB,SAAS;AAC7C,QAAO,SAAS;AAChB,oBAAmB,UAAU,SAAS;AAGtC,KAAI,aAAa,oBACf,KAAI;AACF,qBAAS,QAAQ;UACV,KAAK;AACZ,UAAQ,MAAM,gEAAgE,IAAI;;;;;yBAhOhC;wBAMhB;AAkClC,uBAAsB,KAAK,SAAS,EAAE,eAAe,qBAAqB"}
|
|
1
|
+
{"version":3,"file":"review-status-Bymwzh2i.js","names":[],"sources":["../../src/lib/database/review-status-db.ts","../../src/lib/review-status.ts"],"sourcesContent":["/**\n * Review Status SQLite Storage\n *\n * Provides SQLite-backed CRUD for ReviewStatus, matching the interface in\n * src/lib/review-status.ts. Atomic single-transaction writes eliminate the\n * TOCTOU race in the JSON-backed implementation.\n */\n\nimport { getDatabase } from './index.js';\nimport type { ReviewStatus, StatusHistoryEntry } from '../review-status.js';\n\n// ============== Write operations ==============\n\n/**\n * Upsert a review status record atomically.\n * Replaces the JSON read-modify-write cycle with a single transaction.\n */\nexport function upsertReviewStatus(status: ReviewStatus): void {\n const db = getDatabase();\n\n const upsert = db.transaction((s: ReviewStatus) => {\n // Upsert main record\n db.prepare(`\n INSERT INTO review_status (\n issue_id, review_status, test_status, merge_status,\n verification_status, verification_notes,\n verification_cycle_count, verification_max_cycles,\n review_notes, test_notes, merge_notes,\n updated_at, ready_for_merge, auto_requeue_count, pr_url\n ) VALUES (\n ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?\n )\n ON CONFLICT(issue_id) DO UPDATE SET\n review_status = excluded.review_status,\n test_status = excluded.test_status,\n merge_status = excluded.merge_status,\n verification_status = excluded.verification_status,\n verification_notes = excluded.verification_notes,\n verification_cycle_count = excluded.verification_cycle_count,\n verification_max_cycles = excluded.verification_max_cycles,\n review_notes = excluded.review_notes,\n test_notes = excluded.test_notes,\n merge_notes = excluded.merge_notes,\n updated_at = excluded.updated_at,\n ready_for_merge = excluded.ready_for_merge,\n auto_requeue_count = excluded.auto_requeue_count,\n pr_url = excluded.pr_url\n `).run(\n s.issueId,\n s.reviewStatus,\n s.testStatus,\n s.mergeStatus ?? null,\n s.verificationStatus ?? null,\n s.verificationNotes ?? null,\n s.verificationCycleCount ?? null,\n s.verificationMaxCycles ?? null,\n s.reviewNotes ?? null,\n s.testNotes ?? null,\n s.mergeNotes ?? null,\n s.updatedAt,\n s.readyForMerge ? 1 : 0,\n s.autoRequeueCount ?? null,\n s.prUrl ?? null,\n );\n\n // Append new history entries (deduplicate by timestamp to avoid re-inserting)\n if (s.history && s.history.length > 0) {\n const insertHistory = db.prepare(`\n INSERT OR IGNORE INTO status_history (issue_id, type, status, timestamp, notes)\n VALUES (?, ?, ?, ?, ?)\n `);\n for (const entry of s.history) {\n insertHistory.run(s.issueId, entry.type, entry.status, entry.timestamp, entry.notes ?? null);\n }\n }\n });\n\n upsert(status);\n}\n\n/**\n * Delete a review status record and its history.\n */\nexport function deleteReviewStatus(issueId: string): void {\n const db = getDatabase();\n db.prepare('DELETE FROM review_status WHERE issue_id = ?').run(issueId);\n}\n\n// ============== Read operations ==============\n\n/**\n * Get a single review status by issue ID.\n */\nexport function getReviewStatusFromDb(issueId: string): ReviewStatus | null {\n const db = getDatabase();\n\n const row = db.prepare(`\n SELECT * FROM review_status WHERE issue_id = ?\n `).get(issueId) as DbReviewStatusRow | undefined;\n\n if (!row) return null;\n\n const history = getHistoryFromDb(issueId);\n return rowToReviewStatus(row, history);\n}\n\n/**\n * Get all review statuses.\n */\nexport function getAllReviewStatusesFromDb(): Record<string, ReviewStatus> {\n const db = getDatabase();\n\n const rows = db.prepare('SELECT * FROM review_status ORDER BY updated_at DESC').all() as DbReviewStatusRow[];\n const result: Record<string, ReviewStatus> = {};\n\n for (const row of rows) {\n const history = getHistoryFromDb(row.issue_id);\n result[row.issue_id] = rowToReviewStatus(row, history);\n }\n\n return result;\n}\n\n/**\n * Get history entries for an issue.\n */\nfunction getHistoryFromDb(issueId: string): StatusHistoryEntry[] {\n const db = getDatabase();\n const rows = db.prepare(`\n SELECT type, status, timestamp, notes\n FROM status_history\n WHERE issue_id = ?\n ORDER BY timestamp ASC\n `).all(issueId) as Array<{ type: string; status: string; timestamp: string; notes: string | null }>;\n\n return rows.map(r => ({\n type: r.type as 'review' | 'test' | 'merge',\n status: r.status,\n timestamp: r.timestamp,\n ...(r.notes ? { notes: r.notes } : {}),\n }));\n}\n\n// ============== Row mapping ==============\n\ninterface DbReviewStatusRow {\n issue_id: string;\n review_status: string;\n test_status: string;\n merge_status: string | null;\n verification_status: string | null;\n verification_notes: string | null;\n verification_cycle_count: number | null;\n verification_max_cycles: number | null;\n review_notes: string | null;\n test_notes: string | null;\n merge_notes: string | null;\n updated_at: string;\n ready_for_merge: number;\n auto_requeue_count: number | null;\n pr_url: string | null;\n}\n\nfunction rowToReviewStatus(row: DbReviewStatusRow, history: StatusHistoryEntry[]): ReviewStatus {\n return {\n issueId: row.issue_id,\n reviewStatus: row.review_status as ReviewStatus['reviewStatus'],\n testStatus: row.test_status as ReviewStatus['testStatus'],\n mergeStatus: row.merge_status as ReviewStatus['mergeStatus'] ?? undefined,\n verificationStatus: row.verification_status as ReviewStatus['verificationStatus'] ?? undefined,\n verificationNotes: row.verification_notes ?? undefined,\n verificationCycleCount: row.verification_cycle_count ?? undefined,\n verificationMaxCycles: row.verification_max_cycles ?? undefined,\n reviewNotes: row.review_notes ?? undefined,\n testNotes: row.test_notes ?? undefined,\n mergeNotes: row.merge_notes ?? undefined,\n updatedAt: row.updated_at,\n readyForMerge: row.ready_for_merge === 1,\n autoRequeueCount: row.auto_requeue_count ?? undefined,\n prUrl: row.pr_url ?? undefined,\n history: history.length > 0 ? history : undefined,\n };\n}\n","import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { homedir } from 'os';\nimport { notifyPipeline } from './pipeline-notifier.js';\nimport {\n upsertReviewStatus as dbUpsert,\n deleteReviewStatus as dbDelete,\n getReviewStatusFromDb,\n getAllReviewStatusesFromDb,\n} from './database/review-status-db.js';\n\nexport interface StatusHistoryEntry {\n type: 'review' | 'test' | 'merge' | 'inspect' | 'uat';\n status: string;\n timestamp: string;\n notes?: string;\n}\n\nexport interface ReviewStatus {\n issueId: string;\n reviewStatus: 'pending' | 'reviewing' | 'passed' | 'failed' | 'blocked';\n testStatus: 'pending' | 'testing' | 'passed' | 'failed' | 'skipped' | 'dispatch_failed';\n mergeStatus?: 'pending' | 'merging' | 'merged' | 'failed';\n inspectStatus?: 'pending' | 'inspecting' | 'passed' | 'failed';\n inspectNotes?: string;\n uatStatus?: 'pending' | 'testing' | 'passed' | 'failed';\n uatNotes?: string;\n verificationStatus?: 'pending' | 'running' | 'passed' | 'failed' | 'skipped';\n verificationNotes?: string;\n verificationCycleCount?: number;\n verificationMaxCycles?: number;\n reviewNotes?: string;\n testNotes?: string;\n mergeNotes?: string;\n updatedAt: string;\n readyForMerge: boolean;\n autoRequeueCount?: number;\n prUrl?: string;\n history?: StatusHistoryEntry[];\n /** HEAD commit SHA at the time review passed — used to detect new commits after review */\n reviewedAtCommit?: string;\n}\n\nconst DEFAULT_STATUS_FILE = join(homedir(), '.panopticon', 'review-status.json');\n\nexport function loadReviewStatuses(filePath = DEFAULT_STATUS_FILE): Record<string, ReviewStatus> {\n // Prefer SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n return getAllReviewStatusesFromDb();\n } catch {\n // Fall through to JSON on DB error\n }\n }\n\n try {\n if (existsSync(filePath)) {\n return JSON.parse(readFileSync(filePath, 'utf-8'));\n }\n } catch (err) {\n console.error('Failed to load review statuses:', err);\n }\n return {};\n}\n\nexport function saveReviewStatuses(statuses: Record<string, ReviewStatus>, filePath = DEFAULT_STATUS_FILE): void {\n try {\n const dir = dirname(filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(filePath, JSON.stringify(statuses, null, 2));\n } catch (err) {\n console.error('Failed to save review statuses:', err);\n }\n}\n\nexport function setReviewStatus(\n issueId: string,\n update: Partial<ReviewStatus>,\n filePath = DEFAULT_STATUS_FILE,\n): ReviewStatus {\n const statuses = loadReviewStatuses(filePath);\n const existing = statuses[issueId] || {\n issueId,\n reviewStatus: 'pending' as const,\n testStatus: 'pending' as const,\n updatedAt: new Date().toISOString(),\n readyForMerge: false,\n };\n\n // Guard: reject reviewStatus regression from 'passed' to 'reviewing' unless the caller\n // is explicitly resetting the merge lifecycle (update includes mergeStatus).\n // This is belt-and-suspenders — endpoint-level guards should catch this first.\n if (update.reviewStatus === 'reviewing' && existing.reviewStatus === 'passed' && update.mergeStatus === undefined) {\n console.warn(`[review-status] Rejecting reviewStatus regression from 'passed' to 'reviewing' for ${issueId} (mergeStatus not being reset)`);\n return existing as ReviewStatus;\n }\n\n const merged = { ...existing, ...update };\n\n // Track status transitions in history (last 10 entries)\n const history = [...(existing.history || [])];\n const now = new Date().toISOString();\n if (update.reviewStatus && update.reviewStatus !== existing.reviewStatus) {\n history.push({ type: 'review', status: update.reviewStatus, timestamp: now, notes: update.reviewNotes });\n }\n if (update.testStatus && update.testStatus !== existing.testStatus) {\n history.push({ type: 'test', status: update.testStatus, timestamp: now, notes: update.testNotes });\n }\n if (update.uatStatus && update.uatStatus !== existing.uatStatus) {\n history.push({ type: 'uat', status: update.uatStatus, timestamp: now, notes: update.uatNotes });\n }\n if (update.mergeStatus && update.mergeStatus !== existing.mergeStatus) {\n history.push({ type: 'merge', status: update.mergeStatus, timestamp: now });\n }\n while (history.length > 10) history.shift();\n\n // readyForMerge is true when all required gates pass.\n // If uatStatus exists (UAT specialist has been involved), it must also be 'passed'.\n // verificationStatus must not be 'failed' — verification catches pre-existing test breakage\n // that scoped test runs (e2e/dashboard) may miss.\n const readyForMerge = update.readyForMerge !== undefined\n ? update.readyForMerge\n : (\n merged.reviewStatus === 'passed' &&\n merged.testStatus === 'passed' &&\n merged.verificationStatus !== 'failed' &&\n merged.mergeStatus !== 'merged' &&\n // If UAT has been initiated, it must pass too\n (merged.uatStatus === undefined || merged.uatStatus === 'passed')\n );\n\n const updated: ReviewStatus = {\n ...merged,\n issueId,\n updatedAt: now,\n readyForMerge,\n history,\n };\n\n // Report commit statuses to GitHub when readyForMerge transitions to true (PAN-536)\n if (readyForMerge && !existing.readyForMerge && updated.prUrl) {\n (async () => {\n try {\n const { isGitHubAppConfigured, reportCommitStatus } = await import('./github-app.js');\n if (!isGitHubAppConfigured()) return;\n const prMatch = updated.prUrl!.match(/github\\.com\\/([^/]+)\\/([^/]+)\\/pull/);\n if (!prMatch) return;\n const [, owner, repo] = prMatch;\n // Get HEAD SHA of the PR branch\n const { exec } = await import('child_process');\n const { promisify } = await import('util');\n const execAsync = promisify(exec);\n const { stdout } = await execAsync(\n `gh pr view ${updated.prUrl!.match(/\\/pull\\/(\\d+)/)?.[1]} --json headRefOid --jq .headRefOid`,\n { encoding: 'utf-8', timeout: 10000 }\n );\n const sha = stdout.trim();\n if (sha) {\n await reportCommitStatus(owner, repo, sha, 'success', 'panopticon/review', 'Review passed');\n await reportCommitStatus(owner, repo, sha, 'success', 'panopticon/test', 'Tests passed');\n console.log(`[review-status] Reported commit statuses for ${issueId} (${sha.slice(0, 8)})`);\n }\n } catch (err: any) {\n console.warn(`[review-status] Failed to report commit status: ${err.message}`);\n }\n })();\n }\n\n // SQLite first — it is the authoritative store (reads prefer SQLite)\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n dbUpsert(updated);\n } catch (err) {\n console.error('[review-status] SQLite write failed (continuing with JSON):', err);\n }\n }\n\n // JSON second — legacy fallback for tools that read review-status.json directly\n statuses[issueId] = updated;\n saveReviewStatuses(statuses, filePath);\n\n notifyPipeline({ type: 'status_changed', issueId, status: updated });\n\n // Queue test-agent when review transitions to 'passed'.\n // This fires regardless of how setReviewStatus() is called (API or direct import),\n // ensuring test-agent is queued even when review-agent bypasses the specialist\n // dispatch endpoint. Idempotent — if test-agent is already queued, pushToHook\n // deduplicates by issueId.\n if (\n update.reviewStatus === 'passed' &&\n existing.reviewStatus !== 'passed' &&\n existing.testStatus === 'pending'\n ) {\n (async () => {\n try {\n const { submitToSpecialistQueue } = await import('./cloister/specialists.js');\n const workAgentId = `agent-${issueId.toLowerCase()}`;\n const workStateFile = join(homedir(), '.panopticon', 'agents', workAgentId, 'state.json');\n let workspace: string | undefined;\n let branch: string | undefined;\n if (existsSync(workStateFile)) {\n try {\n const workState = JSON.parse(readFileSync(workStateFile, 'utf-8'));\n workspace = workState.workspace;\n branch = workState.branch || `feature/${issueId.toLowerCase()}`;\n } catch {}\n }\n submitToSpecialistQueue('test-agent', {\n priority: 'high',\n source: 'review-agent-auto',\n issueId,\n workspace,\n branch,\n });\n console.log(`[review-status] Queued test-agent for ${issueId} after review passed`);\n } catch (err: any) {\n console.warn(`[review-status] Failed to queue test-agent for ${issueId}: ${err.message}`);\n }\n })();\n }\n\n // Auto-deliver feedback to work agent when review blocks or tests fail.\n // This ensures feedback reaches the agent regardless of whether status was\n // set via the dashboard API or directly (e.g., bun -e import). See PAN-586.\n if (\n (update.reviewStatus === 'blocked' || update.testStatus === 'failed') &&\n (update.reviewStatus !== existing.reviewStatus || update.testStatus !== existing.testStatus)\n ) {\n const agentSession = `agent-${issueId.toLowerCase()}`;\n (async () => {\n try {\n const { sessionExists } = await import('./tmux.js');\n if (!sessionExists(agentSession)) return;\n\n const statusType = update.reviewStatus === 'blocked' ? 'REVIEW BLOCKED' : 'TESTS FAILED';\n const notes = update.reviewNotes || update.testNotes || 'No details provided.';\n const msg = `SPECIALIST FEEDBACK: ${statusType} for ${issueId}.\\n\\n${notes}\\n\\nFix the issues, then run: pan work done ${issueId}`;\n\n const { messageAgent } = await import('./agents.js');\n await messageAgent(agentSession, msg);\n console.log(`[review-status] Auto-delivered ${statusType} feedback to ${agentSession}`);\n } catch (err: any) {\n console.warn(`[review-status] Failed to auto-deliver feedback to ${agentSession}: ${err.message}`);\n }\n })();\n }\n\n return updated;\n}\n\nexport function getReviewStatus(issueId: string, filePath = DEFAULT_STATUS_FILE): ReviewStatus | null {\n // Prefer SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n const fromDb = getReviewStatusFromDb(issueId);\n if (fromDb) return fromDb;\n } catch {\n // Fall through to JSON on DB error\n }\n }\n const statuses = loadReviewStatuses(filePath);\n return statuses[issueId] || null;\n}\n\n/**\n * On server startup, clear any mergeStatus stuck at 'merging'.\n * Pending merge operations are in-memory only — they don't survive a restart.\n * Any 'merging' status after boot is definitionally stuck (PAN-490).\n */\nexport function clearStuckMergeStatuses(): void {\n const statuses = loadReviewStatuses();\n const stuck = Object.values(statuses).filter(s => s.mergeStatus === 'merging');\n if (stuck.length === 0) return;\n console.log(`[review-status] Clearing ${stuck.length} stuck 'merging' status(es) on startup`);\n for (const s of stuck) {\n setReviewStatus(s.issueId, { mergeStatus: 'pending' });\n }\n}\n\nexport function clearReviewStatus(issueId: string, filePath = DEFAULT_STATUS_FILE): void {\n const statuses = loadReviewStatuses(filePath);\n delete statuses[issueId];\n saveReviewStatuses(statuses, filePath);\n\n // Dual-delete from SQLite when using the default path\n if (filePath === DEFAULT_STATUS_FILE) {\n try {\n dbDelete(issueId);\n } catch (err) {\n console.error('[review-status] SQLite delete failed (continuing with JSON):', err);\n }\n }\n}\n"],"mappings":"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"}
|