@overdeck/core 0.45.10 → 0.45.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-state-KVbJEG5f.js → agent-state-RLhS7ckO.js} +1 -1
- package/dist/{agents-pp8FO7Uj.js → agents-By3u_fQ8.js} +1 -1
- package/dist/{agents-CQHK0CSA.js → agents-CgVr77Qq.js} +12 -12
- package/dist/{agents-CQHK0CSA.js.map → agents-CgVr77Qq.js.map} +1 -1
- package/dist/{backlog-auto-trigger-CyuoS0nl.js → backlog-auto-trigger-B_-1tk4e.js} +5 -5
- package/dist/{backlog-auto-trigger-CyuoS0nl.js.map → backlog-auto-trigger-B_-1tk4e.js.map} +1 -1
- package/dist/cli/index.js +45 -45
- package/dist/{cli-Cewlu3yU.js → cli-DGeSW85B.js} +2 -2
- package/dist/{cli-Cewlu3yU.js.map → cli-DGeSW85B.js.map} +1 -1
- package/dist/cli-MTB9IOoR.js +3 -0
- package/dist/{close-issue-BJGX9_z7.js → close-issue-Djajb6Sv.js} +2 -2
- package/dist/{close-issue-BJGX9_z7.js.map → close-issue-Djajb6Sv.js.map} +1 -1
- package/dist/{concurrency-Ca-fC7tA.js → concurrency-4rzjHAQA.js} +1 -1
- package/dist/{conversation-compaction-B_6s9EvM.js → conversation-compaction-BQvZh09I.js} +4 -4
- package/dist/{conversation-compaction-B_6s9EvM.js.map → conversation-compaction-BQvZh09I.js.map} +1 -1
- package/dist/dashboard/{advancing-selfheal-CMlGznKv.js → advancing-selfheal-BezNfM3P.js} +3 -3
- package/dist/dashboard/{advancing-selfheal-CMlGznKv.js.map → advancing-selfheal-BezNfM3P.js.map} +1 -1
- package/dist/dashboard/{agent-state-xk_G8t90.js → agent-state-BUANcaA7.js} +1 -1
- package/dist/dashboard/{agent-state-service-C1iUF9Ft.js → agent-state-service-DmFpIMbP.js} +1 -1
- package/dist/dashboard/{agent-state-service-5ge9xxhx.js → agent-state-service-tXUdp6UF.js} +2 -2
- package/dist/dashboard/{agent-state-service-5ge9xxhx.js.map → agent-state-service-tXUdp6UF.js.map} +1 -1
- package/dist/dashboard/{agents-BIggLvi_.js → agents-C-vqmKDi.js} +1 -1
- package/dist/dashboard/{agents-DHvsjsuP.js → agents-C7V4odQ6.js} +11 -11
- package/dist/dashboard/{agents-DHvsjsuP.js.map → agents-C7V4odQ6.js.map} +1 -1
- package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js → backlog-auto-trigger-Cb8PR-5w.js} +2 -2
- package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js.map → backlog-auto-trigger-Cb8PR-5w.js.map} +1 -1
- package/dist/dashboard/{cli-CvK9xuSS.js → cli-BgzbB6h3.js} +2 -2
- package/dist/dashboard/{cli-CvK9xuSS.js.map → cli-BgzbB6h3.js.map} +1 -1
- package/dist/dashboard/cli-DCqy_wFa.js +3 -0
- package/dist/dashboard/{close-issue-BnpErnD1.js → close-issue-BWf3pxn4.js} +2 -2
- package/dist/dashboard/{close-issue-BnpErnD1.js.map → close-issue-BWf3pxn4.js.map} +1 -1
- package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js → closed-issue-reaper-D7XPq5pY.js} +4 -4
- package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js.map → closed-issue-reaper-D7XPq5pY.js.map} +1 -1
- package/dist/dashboard/{closed-issue-reaper-BLCQwTpF.js → closed-issue-reaper-VSTSTROK.js} +1 -1
- package/dist/dashboard/{conflict-gate-JdFuhKuF.js → conflict-gate-DwOslTij.js} +1 -1
- package/dist/dashboard/{conflict-gate-BhKvrPxn.js → conflict-gate-FFEvTDVA.js} +3 -3
- package/dist/dashboard/{conflict-gate-BhKvrPxn.js.map → conflict-gate-FFEvTDVA.js.map} +1 -1
- package/dist/dashboard/{conversation-compaction-C5o_eKEm.js → conversation-compaction-CCqN4hWk.js} +4 -4
- package/dist/dashboard/{conversation-compaction-C5o_eKEm.js.map → conversation-compaction-CCqN4hWk.js.map} +1 -1
- package/dist/dashboard/{conversation-compaction-Ba5aZdrb.js → conversation-compaction-CL5Ti4j4.js} +1 -1
- package/dist/dashboard/{deacon-j3qSZ7--.js → deacon-CGHl90eC.js} +107 -50
- package/dist/dashboard/{deacon-j3qSZ7--.js.map → deacon-CGHl90eC.js.map} +1 -1
- package/dist/dashboard/{deacon-Cv2RMfHs.js → deacon-DMPh40Im.js} +2 -2
- package/dist/dashboard/deacon-swarm-CVTiNL0N.js +2 -0
- package/dist/dashboard/{deacon-swarm-DadWNi5J.js → deacon-swarm-aMMJTq6t.js} +4 -4
- package/dist/dashboard/{deacon-swarm-DadWNi5J.js.map → deacon-swarm-aMMJTq6t.js.map} +1 -1
- package/dist/dashboard/deacon.js +4 -4
- package/dist/dashboard/{feedback-target-oE4-Esxl.js → feedback-target-dJjFfITP.js} +4 -4
- package/dist/dashboard/{feedback-target-oE4-Esxl.js.map → feedback-target-dJjFfITP.js.map} +1 -1
- package/dist/dashboard/{feedback-target-C4DaDSZ8.js → feedback-target-nQSM6ygL.js} +1 -1
- package/dist/dashboard/{flywheel-ClMjGW_u.js → flywheel-BLoVNtJV.js} +6 -6
- package/dist/dashboard/{flywheel-ClMjGW_u.js.map → flywheel-BLoVNtJV.js.map} +1 -1
- package/dist/dashboard/{flywheel-DefHRzY4.js → flywheel-Cqf8vu7N.js} +6 -6
- package/dist/dashboard/{flywheel-DefHRzY4.js.map → flywheel-Cqf8vu7N.js.map} +1 -1
- package/dist/dashboard/{flywheel-BqDdGvrQ.js → flywheel-PJsbrXNY.js} +1 -1
- package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js → flywheel-merge-order-B7BH8mZ3.js} +2 -2
- package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js.map → flywheel-merge-order-B7BH8mZ3.js.map} +1 -1
- package/dist/dashboard/{flywheel-merge-order-B6YDSElh.js → flywheel-merge-order-CCwF60b-.js} +1 -1
- package/dist/dashboard/{harness-resolve-DKeSVsJL.js → harness-resolve-B3f9G1Kp.js} +2 -2
- package/dist/dashboard/{harness-resolve-DKeSVsJL.js.map → harness-resolve-B3f9G1Kp.js.map} +1 -1
- package/dist/dashboard/{inspect-agent-BKKc0XnB.js → inspect-agent-CKF5-SJr.js} +3 -3
- package/dist/dashboard/{inspect-agent-BKKc0XnB.js.map → inspect-agent-CKF5-SJr.js.map} +1 -1
- package/dist/dashboard/inspect-agent-CqpEWGWy.js +2 -0
- package/dist/dashboard/{issue-lifecycle-DnhQgddB.js → issue-lifecycle-5LbOcATP.js} +7 -7
- package/dist/dashboard/{issue-lifecycle-DnhQgddB.js.map → issue-lifecycle-5LbOcATP.js.map} +1 -1
- package/dist/dashboard/{issue-lifecycle-nY4esZn8.js → issue-lifecycle-CjYqQ669.js} +1 -1
- package/dist/dashboard/{issue-service-singleton-Bnqv1uaa.js → issue-service-singleton-B8iu_TKV.js} +1 -1
- package/dist/dashboard/{issue-service-singleton-_AquSBTl.js → issue-service-singleton-CDGt__oQ.js} +2 -2
- package/dist/dashboard/{issue-service-singleton-_AquSBTl.js.map → issue-service-singleton-CDGt__oQ.js.map} +1 -1
- package/dist/dashboard/{lifecycle-CGKm7doE.js → lifecycle-CEg4sHZH.js} +4 -4
- package/dist/dashboard/{lifecycle-CGKm7doE.js.map → lifecycle-CEg4sHZH.js.map} +1 -1
- package/dist/dashboard/{merge-agent-Dh8Y9PDO.js → merge-agent-7nBiCytp.js} +1 -1
- package/dist/dashboard/{merge-agent-BcgAYStl.js → merge-agent-C81xPIGd.js} +6 -6
- package/dist/dashboard/{merge-agent-BcgAYStl.js.map → merge-agent-C81xPIGd.js.map} +1 -1
- package/dist/dashboard/merge-ops-DE_6CnmZ.js +2 -0
- package/dist/dashboard/{merge-train-DmxlYbEo.js → merge-train-D3qzRccL.js} +2 -2
- package/dist/dashboard/{merge-train-DmxlYbEo.js.map → merge-train-D3qzRccL.js.map} +1 -1
- package/dist/dashboard/{merge-train-deps-DBHsOWt9.js → merge-train-deps-Be5ElC_P.js} +2 -2
- package/dist/dashboard/{merge-train-deps-DBHsOWt9.js.map → merge-train-deps-Be5ElC_P.js.map} +1 -1
- package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js → orphan-proposed-reconciler-Boq_XaRe.js} +3 -3
- package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js.map → orphan-proposed-reconciler-Boq_XaRe.js.map} +1 -1
- package/dist/dashboard/{orphan-proposed-reconciler-mXV--tcB.js → orphan-proposed-reconciler-C0sPnDq_.js} +1 -1
- package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js → reconstruct-cache-C2pCltOq.js} +3 -3
- package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js.map → reconstruct-cache-C2pCltOq.js.map} +1 -1
- package/dist/dashboard/{remote-completion-Bklh6loJ.js → remote-completion-BmxD5UMQ.js} +3 -3
- package/dist/dashboard/{remote-completion-Bklh6loJ.js.map → remote-completion-BmxD5UMQ.js.map} +1 -1
- package/dist/dashboard/resume-C-nxu9Y4.js +3 -0
- package/dist/dashboard/{review-agent-phzqNqIs.js → review-agent-D6P6d3pz.js} +26 -17
- package/dist/dashboard/review-agent-D6P6d3pz.js.map +1 -0
- package/dist/dashboard/review-agent-DTINEffB.js +3 -0
- package/dist/dashboard/{review-context-CCq56O_Y.js → review-context-Dyo0tsBa.js} +2 -2
- package/dist/dashboard/{review-context-CCq56O_Y.js.map → review-context-Dyo0tsBa.js.map} +1 -1
- package/dist/dashboard/{review-status-BQjqQXZG.js → review-status-B5ZASOo4.js} +18 -5
- package/dist/dashboard/review-status-B5ZASOo4.js.map +1 -0
- package/dist/dashboard/{review-status-bBkqUj5i.js → review-status-h7BXIA4z.js} +1 -1
- package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js → review-verdict-feedback-B70VbfRL.js} +4 -4
- package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js.map → review-verdict-feedback-B70VbfRL.js.map} +1 -1
- package/dist/dashboard/{review-verdict-feedback-cZh9Wi9G.js → review-verdict-feedback-CorgH49f.js} +1 -1
- package/dist/dashboard/{sequence-io-B2GmilSI.js → sequence-io-6YP8fVpH.js} +2 -2
- package/dist/dashboard/{sequence-io-B2GmilSI.js.map → sequence-io-6YP8fVpH.js.map} +1 -1
- package/dist/dashboard/{sequencer-agent-D8pW2aU_.js → sequencer-agent-ja-nkEc1.js} +5 -5
- package/dist/dashboard/{sequencer-agent-D8pW2aU_.js.map → sequencer-agent-ja-nkEc1.js.map} +1 -1
- package/dist/dashboard/server.js +66 -55
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/{service-CILd-uOt.js → service-HKDrIy0h.js} +24 -24
- package/dist/dashboard/{service-CILd-uOt.js.map → service-HKDrIy0h.js.map} +1 -1
- package/dist/dashboard/{smart-compaction-Bgp_gwcA.js → smart-compaction-L0LSnF0_.js} +1 -1
- package/dist/dashboard/{smart-compaction-CrP90OFE.js → smart-compaction-uke1npUz.js} +2 -2
- package/dist/dashboard/{smart-compaction-CrP90OFE.js.map → smart-compaction-uke1npUz.js.map} +1 -1
- package/dist/dashboard/{spawn-planning-session-D15KbPOy.js → spawn-planning-session-CSMWco8B.js} +1 -1
- package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js → spawn-planning-session-CXc0NkwV.js} +3 -3
- package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js.map → spawn-planning-session-CXc0NkwV.js.map} +1 -1
- package/dist/dashboard/{specialist-context-CwKkcXL9.js → specialist-context-BGCJ24LH.js} +3 -3
- package/dist/dashboard/{specialist-context-CwKkcXL9.js.map → specialist-context-BGCJ24LH.js.map} +1 -1
- package/dist/dashboard/{specialist-logs-CvDxquV_.js → specialist-logs--Uz-r8ia.js} +2 -2
- package/dist/dashboard/{specialist-logs-CvDxquV_.js.map → specialist-logs--Uz-r8ia.js.map} +1 -1
- package/dist/dashboard/{specialists-BCjcG4uE.js → specialists-Ca9XSBCS.js} +1 -1
- package/dist/dashboard/{specialists-D07ybnam.js → specialists-DEfrAbWs.js} +36 -36
- package/dist/dashboard/{specialists-D07ybnam.js.map → specialists-DEfrAbWs.js.map} +1 -1
- package/dist/dashboard/{specialists-F2uMO4QS.js → specialists-DeZx9xom.js} +1 -1
- package/dist/dashboard/{specialists-BYi0k8CZ.js → specialists-nZ0mRwpZ.js} +12 -12
- package/dist/dashboard/{specialists-BYi0k8CZ.js.map → specialists-nZ0mRwpZ.js.map} +1 -1
- package/dist/dashboard/{summary-fork-DedqguZb.js → summary-fork-2a1L2DIU.js} +2 -2
- package/dist/dashboard/{summary-fork-EjvOkEPl.js → summary-fork-Xn0QUNmb.js} +3 -3
- package/dist/dashboard/{summary-fork-EjvOkEPl.js.map → summary-fork-Xn0QUNmb.js.map} +1 -1
- package/dist/dashboard/{teardown-workspace-5LCVbjfq.js → teardown-workspace-DhA6oAIK.js} +2 -2
- package/dist/dashboard/{teardown-workspace-5LCVbjfq.js.map → teardown-workspace-DhA6oAIK.js.map} +1 -1
- package/dist/dashboard/{test-agent-queue-qfsq6uaC.js → test-agent-queue-B_ukPyIK.js} +3 -3
- package/dist/dashboard/{test-agent-queue-qfsq6uaC.js.map → test-agent-queue-B_ukPyIK.js.map} +1 -1
- package/dist/dashboard/{uat-train-CtNGgXxj.js → uat-train-BLBAbJ8Y.js} +10 -10
- package/dist/dashboard/{uat-train-CtNGgXxj.js.map → uat-train-BLBAbJ8Y.js.map} +1 -1
- package/dist/dashboard/{uat-train-C3hWXtip.js → uat-train-DPfGR9_I.js} +1 -1
- package/dist/dashboard/{verification-runner-DuZRHjPU.js → verification-runner-KTGTNugM.js} +1 -1
- package/dist/dashboard/{verification-runner-BPAhPWR7.js → verification-runner-R2-4XgMO.js} +4 -4
- package/dist/dashboard/{verification-runner-BPAhPWR7.js.map → verification-runner-R2-4XgMO.js.map} +1 -1
- package/dist/dashboard/{workflows-H-COJ84X.js → workflows-CSKX5MhC.js} +7 -7
- package/dist/dashboard/{workflows-H-COJ84X.js.map → workflows-CSKX5MhC.js.map} +1 -1
- package/dist/dashboard/workflows-DVwXauef.js +2 -0
- package/dist/dashboard/{workspaces-CRU9_vRn.js → workspaces-DXLDgXo2.js} +32 -32
- package/dist/dashboard/{workspaces-CRU9_vRn.js.map → workspaces-DXLDgXo2.js.map} +1 -1
- package/dist/docs-index.sqlite +0 -0
- package/dist/{feedback-target-DTGAhFkM.js → feedback-target-BsKN4Tr_.js} +4 -4
- package/dist/{feedback-target-DTGAhFkM.js.map → feedback-target-BsKN4Tr_.js.map} +1 -1
- package/dist/{feedback-target-DutF088V.js → feedback-target-DM1WzOKy.js} +1 -1
- package/dist/{harness-resolve-Bz1prk6B.js → harness-resolve-B-YDNwNW.js} +2 -2
- package/dist/{harness-resolve-Bz1prk6B.js.map → harness-resolve-B-YDNwNW.js.map} +1 -1
- package/dist/{issue-lifecycle-fVqvCdKM.js → issue-lifecycle-Bc1l3U9f.js} +7 -7
- package/dist/{issue-lifecycle-fVqvCdKM.js.map → issue-lifecycle-Bc1l3U9f.js.map} +1 -1
- package/dist/{issue-service-singleton-CZm8p6Rj.js → issue-service-singleton-D-8Q6tdT.js} +1 -1
- package/dist/{issue-service-singleton-Cr73JAaR.js → issue-service-singleton-D1433I7n.js} +2 -2
- package/dist/{issue-service-singleton-Cr73JAaR.js.map → issue-service-singleton-D1433I7n.js.map} +1 -1
- package/dist/{lifecycle-CR6tZfl1.js → lifecycle-SmrqysDf.js} +7 -7
- package/dist/{lifecycle-CR6tZfl1.js.map → lifecycle-SmrqysDf.js.map} +1 -1
- package/dist/{lifecycle-D_EWiUjk.js → lifecycle-mRMyiIXs.js} +2 -2
- package/dist/{merge-agent-LM4avOtA.js → merge-agent-B9hIZ6Z0.js} +6 -6
- package/dist/{merge-agent-LM4avOtA.js.map → merge-agent-B9hIZ6Z0.js.map} +1 -1
- package/dist/{merge-agent-Bj046HE1.js → merge-agent-DUj22cXL.js} +1 -1
- package/dist/{merge-fulfillment-DrnC1Kmg.js → merge-fulfillment-__P4WE-T.js} +3 -3
- package/dist/{merge-fulfillment-DrnC1Kmg.js.map → merge-fulfillment-__P4WE-T.js.map} +1 -1
- package/dist/{reconstruct-cache-NPqoAUg-.js → reconstruct-cache-9SWp82SQ.js} +3 -3
- package/dist/{reconstruct-cache-NPqoAUg-.js.map → reconstruct-cache-9SWp82SQ.js.map} +1 -1
- package/dist/{records-BIkN1LQ4.js → records-CTmvuRzz.js} +14 -1
- package/dist/{records-BIkN1LQ4.js.map → records-CTmvuRzz.js.map} +1 -1
- package/dist/records-backfill-BQcmTmwF.js +2 -0
- package/dist/{records-backfill-HM0Pln6N.js → records-backfill-BrNsv_zT.js} +2 -2
- package/dist/{records-backfill-HM0Pln6N.js.map → records-backfill-BrNsv_zT.js.map} +1 -1
- package/dist/resume-Bn4kDk9x.js +3 -0
- package/dist/{review-agent-DFosQzCG.js → review-agent-PhqOhpx3.js} +24 -16
- package/dist/{review-agent-DFosQzCG.js.map → review-agent-PhqOhpx3.js.map} +1 -1
- package/dist/review-agent-hLbRRxZF.js +3 -0
- package/dist/{review-status-iJwfEoNX.js → review-status-B_XXomV0.js} +6 -6
- package/dist/{review-status-iJwfEoNX.js.map → review-status-B_XXomV0.js.map} +1 -1
- package/dist/{review-status-BbNVlXh1.js → review-status-Bo-EFVKh.js} +1 -1
- package/dist/{review-verdict-feedback-BGPw3z1X.js → review-verdict-feedback-CXHkoiKZ.js} +1 -1
- package/dist/{review-verdict-feedback-CCZ05atV.js → review-verdict-feedback-DGV13LDb.js} +4 -4
- package/dist/{review-verdict-feedback-CCZ05atV.js.map → review-verdict-feedback-DGV13LDb.js.map} +1 -1
- package/dist/{sequence-io-CcxZNHcD.js → sequence-io-Cg4NHAco.js} +1 -1
- package/dist/{sequence-io-Cx1usxG3.js → sequence-io-CyxvNHfA.js} +2 -2
- package/dist/{sequence-io-Cx1usxG3.js.map → sequence-io-CyxvNHfA.js.map} +1 -1
- package/dist/{smart-compaction-ByUEgQkF.js → smart-compaction-ByKXTQNy.js} +2 -2
- package/dist/{smart-compaction-ByUEgQkF.js.map → smart-compaction-ByKXTQNy.js.map} +1 -1
- package/dist/{smart-compaction-DfLe_CtH.js → smart-compaction-U6e_uB1E.js} +1 -1
- package/dist/{specialist-context-McuGkaeR.js → specialist-context-BUTadO-U.js} +3 -3
- package/dist/{specialist-context-McuGkaeR.js.map → specialist-context-BUTadO-U.js.map} +1 -1
- package/dist/{specialist-logs-CUUJ4eiB.js → specialist-logs-pEMCTtGZ.js} +2 -2
- package/dist/{specialist-logs-CUUJ4eiB.js.map → specialist-logs-pEMCTtGZ.js.map} +1 -1
- package/dist/{specialists-BSrylD1S.js → specialists-BGFnrQ8n.js} +1 -1
- package/dist/{specialists-B0elgInc.js → specialists-DeTkkp8b.js} +12 -12
- package/dist/{specialists-B0elgInc.js.map → specialists-DeTkkp8b.js.map} +1 -1
- package/dist/{summary-fork-DskMRnyb.js → summary-fork-CgzThE2-.js} +2 -2
- package/dist/{summary-fork-CPUTKcAT.js → summary-fork-Dx1RMuvU.js} +3 -3
- package/dist/{summary-fork-CPUTKcAT.js.map → summary-fork-Dx1RMuvU.js.map} +1 -1
- package/dist/{sync-BiNVQHBf.js → sync-Bsww9tuj.js} +2 -2
- package/dist/{sync-BiNVQHBf.js.map → sync-Bsww9tuj.js.map} +1 -1
- package/dist/sync-CylE_SjB.js +2 -0
- package/dist/{verdict-restore-DQiV7_fe.js → verdict-restore-DGpX0ZOJ.js} +5 -5
- package/dist/{verdict-restore-DQiV7_fe.js.map → verdict-restore-DGpX0ZOJ.js.map} +1 -1
- package/dist/{webhook-handlers-DHBSEDEm.js → webhook-handlers-BjsfIL7u.js} +3 -3
- package/dist/{webhook-handlers-DHBSEDEm.js.map → webhook-handlers-BjsfIL7u.js.map} +1 -1
- package/dist/{workspace-migrate-duUsWNpd.js → workspace-migrate-BBQ8nAnP.js} +2 -2
- package/dist/{workspace-migrate-duUsWNpd.js.map → workspace-migrate-BBQ8nAnP.js.map} +1 -1
- package/dist/{workspace-reap-DLCIoJmn.js → workspace-reap-12O9354k.js} +2 -2
- package/dist/{workspace-reap-DLCIoJmn.js.map → workspace-reap-12O9354k.js.map} +1 -1
- package/package.json +1 -1
- package/scripts/file-size-baseline.txt +1 -1
- package/dist/cli-DBWG12XK.js +0 -3
- package/dist/dashboard/cli-CJDWXE_Y.js +0 -3
- package/dist/dashboard/deacon-swarm-D1iAUX-P.js +0 -2
- package/dist/dashboard/inspect-agent-vm88mLYr.js +0 -2
- package/dist/dashboard/merge-ops-QdBiQ8XB.js +0 -2
- package/dist/dashboard/resume-DnIsmIgC.js +0 -3
- package/dist/dashboard/review-agent-DgySDb-G.js +0 -3
- package/dist/dashboard/review-agent-phzqNqIs.js.map +0 -1
- package/dist/dashboard/review-status-BQjqQXZG.js.map +0 -1
- package/dist/dashboard/workflows-Cz8OtiB4.js +0 -2
- package/dist/records-backfill-DEgtRyKL.js +0 -2
- package/dist/resume-DGymuXZM.js +0 -3
- package/dist/review-agent-DjVadURa.js +0 -3
- package/dist/sync-4jqukkxz.js +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-agent-LM4avOtA.js","names":["execAsync"],"sources":["../src/lib/cloister/validation.ts","../src/lib/overdeck/git-activity.ts","../src/lib/git-activity.ts","../src/lib/git/operations.ts","../src/lib/cloister/merge-agent.ts"],"sourcesContent":["/**\n * Merge Validation - Validation utilities for merge completeness\n *\n * Validates that merged code:\n * - Has no conflict markers\n * - Builds successfully\n * - Passes all tests\n */\n\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { join } from 'path';\nimport { existsSync } from 'fs';\nimport { Effect } from 'effect';\nimport type { QualityGateConfig, TemplatePlaceholders } from '../workspace-config.js';\nimport { replacePlaceholdersSync } from '../workspace-config.js';\nimport { loadConfigSync } from '../config.js';\nimport { GitError } from '../errors.js';\n\nconst execAsync = promisify(exec);\nconst DASHBOARD_RUNTIME_ENV_KEYS = ['API_PORT', 'PORT', 'DASHBOARD_URL'] as const;\n\nfunction buildQualityGateEnv(gateEnv: Record<string, string> | undefined): NodeJS.ProcessEnv {\n const env = { ...process.env };\n for (const key of DASHBOARD_RUNTIME_ENV_KEYS) {\n if (!Object.prototype.hasOwnProperty.call(gateEnv ?? {}, key)) {\n delete env[key];\n }\n }\n // PAN-1678: verification and ship builds run `npm run build` as a quality\n // gate, which by default includes the heavy ~10-core build:docs-index job\n // (a docs *search* index, irrelevant to whether the issue's code\n // compiles/tests). Run concurrently across agents it stampedes the host to\n // OOM. Default it off for every gate command so gate builds stay cheap; a\n // gate may still opt back in via gate.env. The skip mechanism lives in\n // scripts/build-post-cli.mjs (PAN-1659). Release/publish builds the index\n // through a separate path (build-for-publish.mjs) that never sets this.\n return { SKIP_DOCS_INDEX: '1', ...env, ...gateEnv };\n}\n\n/**\n * Context for validation execution\n */\nexport interface ValidationContext {\n /** Project root path */\n projectPath: string;\n /** Issue ID for logging */\n issueId?: string;\n /** Custom validation script path (defaults to scripts/validate-merge.sh) */\n validationScript?: string;\n /** Baseline test failure count for comparison mode (pre-existing failures) */\n baselineTestFailures?: number;\n}\n\n/**\n * Detailed validation failure information\n */\nexport interface ValidationFailure {\n /** Type of failure: conflict, build, or test */\n type: 'conflict' | 'build' | 'test';\n /** Files affected (for conflicts) */\n files?: string[];\n /** Error message or output */\n message: string;\n}\n\n/**\n * Result of validation execution\n */\nexport interface ValidationResult {\n /** Overall validation success */\n success: boolean;\n /** Validation passed (or skipped — check `skipped` to distinguish) */\n valid: boolean;\n /** Validation was skipped (no validation script found) */\n skipped?: boolean;\n /** Conflict markers detected */\n conflictMarkersFound: boolean;\n /** Build result */\n buildPassed: boolean | null; // null if not run\n /** Test result */\n testsPassed: boolean | null; // null if not run\n /** List of failures */\n failures: ValidationFailure[];\n /** Raw validation output */\n output: string;\n /** Error message if validation script itself failed */\n error?: string;\n}\n\n/**\n * Parse validation script output to extract structured results\n */\nfunction parseValidationOutput(output: string, exitCode: number): ValidationResult {\n const lines = output.split('\\n');\n\n const failures: ValidationFailure[] = [];\n let conflictMarkersFound = false;\n let buildPassed: boolean | null = null;\n let testsPassed: boolean | null = null;\n\n // Track what stage we're in\n let inConflictCheck = false;\n let inBuildCheck = false;\n let inTestCheck = false;\n\n const conflictFiles: string[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Detect stages\n if (trimmed.startsWith('Checking for conflict markers')) {\n inConflictCheck = true;\n inBuildCheck = false;\n inTestCheck = false;\n } else if (trimmed.startsWith('Running build')) {\n inConflictCheck = false;\n inBuildCheck = true;\n inTestCheck = false;\n } else if (trimmed.startsWith('Running tests')) {\n inConflictCheck = false;\n inBuildCheck = false;\n inTestCheck = true;\n }\n\n // Parse conflict markers\n if (inConflictCheck) {\n if (trimmed.startsWith('ERROR: Conflict')) {\n conflictMarkersFound = true;\n } else if (trimmed.includes('/') && !trimmed.startsWith('ERROR')) {\n // File path listed\n conflictFiles.push(trimmed);\n } else if (trimmed.startsWith('✓ No conflict markers found')) {\n conflictMarkersFound = false;\n }\n }\n\n // Parse build result\n if (inBuildCheck) {\n if (trimmed.startsWith('✓ Build passed')) {\n buildPassed = true;\n } else if (trimmed.startsWith('ERROR: Build failed') ||\n trimmed.includes('VALIDATION FAILED: Build errors detected')) {\n buildPassed = false;\n } else if (trimmed.includes('skipping build check')) {\n buildPassed = null; // Not applicable\n }\n }\n\n // Parse test result\n if (inTestCheck) {\n if (trimmed.startsWith('✓ Tests passed')) {\n testsPassed = true;\n } else if (trimmed.startsWith('ERROR: Tests failed') ||\n trimmed.includes('VALIDATION FAILED: Test failures detected')) {\n testsPassed = false;\n } else if (trimmed.includes('skipping test check')) {\n testsPassed = null; // Not applicable\n }\n }\n }\n\n // Build failures list\n if (conflictMarkersFound) {\n failures.push({\n type: 'conflict',\n files: conflictFiles.length > 0 ? conflictFiles : undefined,\n message: 'Conflict markers detected in merged code',\n });\n }\n\n if (buildPassed === false) {\n failures.push({\n type: 'build',\n message: 'Build failed after merge',\n });\n }\n\n if (testsPassed === false) {\n failures.push({\n type: 'test',\n message: 'Tests failed after merge',\n });\n }\n\n // Determine overall validity\n const valid = exitCode === 0 &&\n !conflictMarkersFound &&\n (buildPassed === null || buildPassed === true) &&\n (testsPassed === null || testsPassed === true);\n\n return {\n success: true, // Script ran successfully\n valid,\n conflictMarkersFound,\n buildPassed,\n testsPassed,\n failures,\n output,\n };\n}async function runMergeValidationPromise(\n context: ValidationContext\n): Promise<ValidationResult> {\n const { projectPath, validationScript } = context;\n\n // Determine validation script path\n const scriptPath = validationScript || join(projectPath, 'scripts', 'validate-merge.sh');\n\n // No validation script = skip validation (specialist already ran build + tests)\n if (!existsSync(scriptPath)) {\n console.log(`[validation] No validation script at ${scriptPath}, skipping (specialist already validated)`);\n return {\n success: true,\n valid: true,\n skipped: true,\n conflictMarkersFound: false,\n buildPassed: null,\n testsPassed: null,\n failures: [],\n output: '',\n };\n }\n\n console.log(`[validation] Running validation script: ${scriptPath}`);\n console.log(`[validation] Project path: ${projectPath}`);\n\n try {\n // Run validation script\n // Pass baseline failures as env var for baseline comparison mode\n const env = { ...process.env };\n if (context.baselineTestFailures !== undefined) {\n env.BASELINE_FAILURES = String(context.baselineTestFailures);\n console.log(`[validation] Baseline comparison mode: ${context.baselineTestFailures} pre-existing failures`);\n }\n\n const { stdout, stderr } = await execAsync(\n `bash \"${scriptPath}\" \"${projectPath}\"`,\n {\n cwd: projectPath,\n env,\n maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs\n timeout: 10 * 60 * 1000, // 10 minute timeout\n }\n );\n\n const output = stdout + stderr;\n\n console.log(`[validation] ✓ Validation passed`);\n\n return parseValidationOutput(output, 0);\n } catch (error: any) {\n // Validation script exited with non-zero code (validation failed)\n const exitCode = error.code || 1;\n const output = (error.stdout || '') + (error.stderr || '');\n\n console.log(`[validation] ✗ Validation failed (exit code ${exitCode})`);\n\n // Parse the output to understand what failed\n const result = parseValidationOutput(output, exitCode);\n\n return result;\n }\n}async function autoRevertMergePromise(projectPath: string): Promise<boolean> {\n console.log(`[validation] Auto-reverting merge in ${projectPath}`);\n\n try {\n // Get current commit before revert (for logging)\n const { stdout: beforeCommit } = await execAsync('git rev-parse HEAD', {\n cwd: projectPath,\n });\n\n // Use ORIG_HEAD — git sets this to pre-merge HEAD at merge time.\n // Handles fast-forwards, multi-commit merges, and any commits\n // added to main between task start and merge execution.\n await execAsync('git reset --hard ORIG_HEAD', {\n cwd: projectPath,\n });\n\n // Get new HEAD after revert (for logging)\n const { stdout: afterCommit } = await execAsync('git rev-parse HEAD', {\n cwd: projectPath,\n });\n\n console.log(\n `[validation] ✓ Auto-revert successful: ${beforeCommit.trim()} -> ${afterCommit.trim()} (via ORIG_HEAD)`\n );\n\n return true;\n } catch (error: any) {\n console.error(`[validation] ✗ Auto-revert failed:`, error.message);\n return false;\n }\n}\n\n/**\n * Result of a single quality gate execution\n */\nexport interface QualityGateResult {\n /** Gate name from projects.yaml */\n name: string;\n /** Whether the gate passed */\n passed: boolean;\n /** Whether the gate was required */\n required: boolean;\n /** Gate output (stdout + stderr) */\n output: string;\n /** Duration in milliseconds */\n durationMs: number;\n /** Error message if gate failed */\n error?: string;\n /** PAN-2461: the gate could not run at all (e.g. its container is not running).\n * Callers must treat this as retryable infrastructure failure — never as a\n * code failure, and never count it against verification attempt budgets. */\n infraUnavailable?: boolean;\n}\n\n/**\n * Options for running quality gates\n */\nexport interface QualityGateRunOptions {\n /** Whether the workspace is remote (SSH) */\n isRemote?: boolean;\n /** VM name for SSH connections (required when isRemote is true) */\n vmName?: string;\n /** Template placeholders for resolving container names (e.g., {{FEATURE_FOLDER}}) */\n placeholders?: TemplatePlaceholders;\n /** PAN-2487: mirror of the human-readable gate progress lines (start/pass/fail\n * per gate), e.g. into the per-issue ship log so the UI can show live progress. */\n onLog?: (line: string) => void;\n}\n\n/**\n * Default quality gates used when no quality_gates config exists in projects.yaml.\n * Runs typecheck → lint → changed-file tests sequentially (bail on first failure).\n * Heavy e2e/browser suites belong in project-specific CI-only or @slow tiers.\n */\nexport const DEFAULT_GATES: Record<string, QualityGateConfig> = {\n typecheck: { command: 'npm run typecheck 2>&1' },\n lint: { command: 'npm run lint 2>&1' },\n test: {\n command: 'OVERDECK_VERIFICATION=1 npx vitest run --changed {{CHANGED_BASE}} && npm --prefix ./src/dashboard/frontend run test -- src/lib/__tests__/issueActions.test.ts src/lib/__tests__/issueActions.no-actions-lost.test.ts src/lib/__tests__/issueActions.parity.test.tsx',\n },\n};async function runQualityGatesPromise(\n gates: Record<string, QualityGateConfig>,\n projectPath: string,\n phase: 'pre_push' | 'post_push' = 'pre_push',\n opts: QualityGateRunOptions = {}\n): Promise<QualityGateResult[]> {\n if (opts.isRemote && !opts.vmName) {\n throw new Error('Remote workspace requires vmName');\n }\n if (opts.isRemote && opts.vmName) {\n // Validate vmName and projectPath to prevent shell injection.\n // Both are controlled by Overdeck config, but explicit validation\n // catches any accidental or malicious values before they reach the shell.\n if (!/^[a-z0-9][a-z0-9-]*$/.test(opts.vmName)) {\n throw new Error(`Invalid vmName for SSH: ${opts.vmName}`);\n }\n if (!/^[a-zA-Z0-9/_\\-.]+$/.test(projectPath)) {\n throw new Error(`Workspace path contains unsafe characters: ${projectPath}`);\n }\n }\n const results: QualityGateResult[] = [];\n\n for (const [name, gate] of Object.entries(gates)) {\n const gatePhase = gate.phase || 'pre_push';\n if (gatePhase !== phase) continue;\n\n const required = gate.required !== false; // default true\n const cwd = gate.path ? join(projectPath, gate.path) : projectPath;\n\n console.log(`[quality-gate] Running \"${name}\" (${required ? 'required' : 'optional'}) in ${cwd}`);\n opts.onLog?.(`Running gate \"${name}\"${required ? '' : ' (optional)'}…`);\n const startTime = Date.now();\n\n if (gate.type === 'http_health') {\n // HTTP health check gate\n const result = await runHttpHealthGate(name, gate, required);\n results.push(result);\n if (!result.passed && required) {\n console.log(`[quality-gate] ✗ Required gate \"${name}\" failed — stopping`);\n break;\n }\n continue;\n }\n\n // Command gate (default)\n\n // PAN-666: substitute {{...}} placeholders into the gate command before\n // resolving it for any execution path. This is what lets a test gate opt into\n // changed-file scoping via `vitest run --changed {{CHANGED_BASE}}` so it runs\n // only the tests affected by the PR and skips pre-existing failures in files\n // the change never touched.\n const command = opts.placeholders\n ? replacePlaceholdersSync(gate.command, opts.placeholders)\n : gate.command;\n\n // For remote workspaces, build and validate the SSH command BEFORE entering\n // the try/catch so validation errors propagate as real errors (not gate failures).\n const isRemote = opts.isRemote && opts.vmName;\n let resolvedCommand: string;\n if (isRemote) {\n // Validate cwd (which may include gate.path) — not just the base projectPath.\n // A gate.path like \"frontend;rm -rf /\" would produce an unsafe cwd after join.\n if (!/^[a-zA-Z0-9/_\\-.]+$/.test(cwd)) {\n throw new Error(`Gate \"${name}\" path resolves to unsafe characters for SSH: ${cwd}`);\n }\n // Validate the command doesn't contain double quotes — a \" in the command would\n // end the SSH double-quoted string and allow local command injection:\n // ssh host \"cd /path && legit; injected\" ← breaks when command contains \"\n if (command.includes('\"')) {\n throw new Error(`Gate \"${name}\" command contains double quotes which are unsafe in SSH context`);\n }\n const flyAppName = loadConfigSync().remote?.fly?.app ?? 'pan-workspaces';\n resolvedCommand = `fly ssh console -a ${flyAppName} -C \"cd ${cwd} && ${command}\"`;\n } else if (gate.container && gate.container_name) {\n // Run inside Docker container — resolve container name from placeholders\n let containerName = gate.container_name;\n if (opts.placeholders) {\n containerName = replacePlaceholdersSync(containerName, opts.placeholders);\n }\n // PAN-2461: a missing container previously burned verification attempts as a\n // fake check failure (\"frontend-lint failed\" in 33ms because docker exec had\n // no target). Pre-check and report infra-unavailable instead.\n try {\n const { stdout: liveCheck } = await execAsync(\n `docker ps --filter \"name=^${containerName}$\" --format \"{{.Names}}\"`,\n { timeout: 10_000 },\n );\n if (!liveCheck.split('\\n').map(s => s.trim()).includes(containerName)) {\n console.log(`[quality-gate] ⚠ \"${name}\" skipped: container ${containerName} is not running (infra-unavailable, does not count as a check failure)`);\n results.push({\n name, passed: false, required,\n output: `container ${containerName} is not running`,\n durationMs: Date.now() - startTime,\n error: `container ${containerName} is not running`,\n infraUnavailable: true,\n });\n if (required) break;\n continue;\n }\n } catch {\n // docker itself unreachable — same treatment.\n results.push({\n name, passed: false, required,\n output: 'docker daemon unreachable',\n durationMs: Date.now() - startTime,\n error: 'docker daemon unreachable',\n infraUnavailable: true,\n });\n if (required) break;\n continue;\n }\n // Use -w to set working directory inside the container.\n // The container mounts workspace code at /workspaces/feature/<subdir>,\n // so map the gate.path (e.g., 'fe') to the container's working directory.\n const containerWorkdir = gate.path ? `/workspaces/feature/${gate.path}` : '/workspaces/feature';\n // Pass gate.env as -e flags so env vars reach the container process\n const envFlags = gate.env\n ? Object.entries(gate.env).map(([k, v]) => `-e ${k}=\"${v}\"`).join(' ')\n : '';\n resolvedCommand = `docker exec ${envFlags} -w \"${containerWorkdir}\" \"${containerName}\" ${command}`;\n console.log(`[quality-gate] Running in container: ${containerName} (workdir: ${containerWorkdir})`);\n } else {\n // Wrap the local gate in `nice -n 19 sh -c '<cmd>'` so a CPU-bound\n // build (tsdown/vite) can't starve the dashboard process that is\n // its parent — when it does, the event-loop tick check reports\n // 600ms+ \"stalls\" that are really just scheduler delay.\n if (process.platform === 'win32') {\n resolvedCommand = command;\n } else {\n const escaped = command.replace(/'/g, `'\\\\''`);\n resolvedCommand = `nice -n 19 sh -c '${escaped}'`;\n }\n }\n\n // PAN-2461: gates may declare `retry: N` — CI-parity for suites with known\n // order-dependent flakes. A pass on any attempt is a pass.\n const maxAttempts = 1 + Math.max(0, gate.retry ?? 0);\n let lastError: any = null;\n let passedGate = false;\n let passOutput = '';\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n // When running in container, don't set host cwd (irrelevant)\n const useHostCwd = !isRemote && !(gate.container && gate.container_name);\n const env = buildQualityGateEnv(gate.env);\n const { stdout, stderr } = await execAsync(resolvedCommand, {\n cwd: useHostCwd ? cwd : undefined,\n env,\n maxBuffer: 10 * 1024 * 1024, // 10MB\n timeout: 20 * 60 * 1000, // 20 minute timeout per gate (PAN-1989: a near-total rename makes `vitest --changed` select ~the whole suite — 715 root + 166 frontend test files — which exceeds 10 min)\n });\n passedGate = true;\n passOutput = (stdout + stderr).slice(-2000); // keep last 2KB\n break;\n } catch (error: any) {\n lastError = error;\n if (attempt < maxAttempts) {\n console.log(`[quality-gate] ✗ \"${name}\" failed attempt ${attempt}/${maxAttempts} — retrying (gate declares retry: ${gate.retry})`);\n opts.onLog?.(`↻ ${name} failed attempt ${attempt}/${maxAttempts} — retrying`);\n }\n }\n }\n\n if (passedGate) {\n const durationMs = Date.now() - startTime;\n console.log(`[quality-gate] ✓ \"${name}\" passed (${durationMs}ms)`);\n opts.onLog?.(`✓ ${name} passed (${Math.round(durationMs/1000)}s)`);\n results.push({\n name,\n passed: true,\n required,\n output: passOutput,\n durationMs,\n });\n } else {\n const error: any = lastError;\n const durationMs = Date.now() - startTime;\n const output = ((error.stdout || '') + (error.stderr || '')).slice(-2000);\n console.log(`[quality-gate] ✗ \"${name}\" failed (${durationMs}ms): ${error.message?.slice(0, 200)}`);\n opts.onLog?.(`✗ ${name} FAILED (${Math.round(durationMs/1000)}s): ${error.message?.slice(0, 160)}`);\n results.push({\n name,\n passed: false,\n required,\n output,\n durationMs,\n error: error.message?.slice(0, 500),\n });\n\n if (required) {\n console.log(`[quality-gate] ✗ Required gate \"${name}\" failed — stopping`);\n break;\n }\n }\n }\n\n const passed = results.filter(r => r.passed).length;\n const failed = results.filter(r => !r.passed).length;\n console.log(`[quality-gate] Complete: ${passed} passed, ${failed} failed out of ${results.length} gates`);\n\n return results;\n}\n\n/**\n * Run an HTTP health check gate (for post-push deployment verification)\n */\nasync function runHttpHealthGate(\n name: string,\n gate: QualityGateConfig,\n required: boolean\n): Promise<QualityGateResult> {\n const url = gate.url;\n if (!url) {\n return {\n name,\n passed: false,\n required,\n output: '',\n durationMs: 0,\n error: 'http_health gate missing url',\n };\n }\n\n const waitSeconds = gate.wait || 120;\n const expectStatus = gate.expect_status || 200;\n const startTime = Date.now();\n\n console.log(`[quality-gate] Waiting ${waitSeconds}s for deployment, then checking ${url}`);\n\n // Wait for deployment\n await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));\n\n try {\n const { stdout } = await execAsync(\n `curl -sL -o /dev/null -w '%{http_code}' --max-time 30 '${url}'`,\n { timeout: 60 * 1000 }\n );\n\n const statusCode = parseInt(stdout.trim(), 10);\n const passed = statusCode === expectStatus;\n const durationMs = Date.now() - startTime;\n\n console.log(`[quality-gate] Health check ${url}: ${statusCode} (expected ${expectStatus}) — ${passed ? 'PASS' : 'FAIL'}`);\n\n return {\n name,\n passed,\n required,\n output: `HTTP ${statusCode} from ${url}`,\n durationMs,\n error: passed ? undefined : `Expected HTTP ${expectStatus}, got ${statusCode}`,\n };\n } catch (error: any) {\n return {\n name,\n passed: false,\n required,\n output: error.message || '',\n durationMs: Date.now() - startTime,\n error: `Health check failed: ${error.message?.slice(0, 200)}`,\n };\n }\n}\n\n// ─── Effect variants (PAN-1249) ──────────────────────────────────────────────\n\n/**\n * Effect variant of {@link runMergeValidation}. The Promise version swallows\n * its own errors and returns a structured {@link ValidationResult}, so the\n * Effect form simply lifts it via `Effect.promise`.\n */\nexport const runMergeValidation = (\n context: ValidationContext,\n): Effect.Effect<ValidationResult> =>\n Effect.promise(() => runMergeValidationPromise(context));\n\n/**\n * Effect variant of {@link autoRevertMerge}. Surfaces git failure through a\n * typed {@link GitError} channel instead of returning `false` silently.\n */\nexport const autoRevertMerge = (\n projectPath: string,\n): Effect.Effect<void, GitError> =>\n Effect.tryPromise({\n try: async () => {\n const ok = await autoRevertMergePromise(projectPath);\n if (!ok) throw new Error('autoRevertMerge returned false');\n },\n catch: (cause) =>\n new GitError({\n command: ['git', 'reset', '--hard', 'ORIG_HEAD'],\n stderr: cause instanceof Error ? cause.message : String(cause),\n exitCode: -1,\n cause,\n }),\n });\n\n/**\n * Effect variant of {@link runQualityGates}. Wraps the Promise implementation\n * with `Effect.promise` because the existing function already aggregates per-\n * gate failures into the returned array — it does not throw on gate failure.\n */\nexport const runQualityGates = (\n gates: Record<string, QualityGateConfig>,\n projectPath: string,\n phase: 'pre_push' | 'post_push' = 'pre_push',\n opts: QualityGateRunOptions = {},\n): Effect.Effect<QualityGateResult[]> =>\n Effect.promise(() => runQualityGatesPromise(gates, projectPath, phase, opts));\n","/**\n * Overdeck door for git_operations — persistent log of git push/fetch/merge\n * events. Replaces database/git-operations-db.ts.\n *\n * Uses the hand-maintained overdeck migration as the schema source of truth.\n */\n\nimport { getOverdeckDatabaseSync } from './infra.js';\n\n// ─── Schema bootstrap ─────────────────────────────────────────────────────────\n\nfunction overdeckDb() {\n return getOverdeckDatabaseSync();\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type GitOperationType =\n | 'push'\n | 'force_push'\n | 'fetch'\n | 'merge'\n | 'rev_parse'\n | 'main_diverged'\n | 'push_attempt'\n | 'fetch_attempt'\n | 'push_rejected'\n | 'non_ff'\n | 'force_push_cmd'\n | 'retry'\n | 'remote_rejected'\n | 'push_noop';\n\nexport type GitOperationStatus = 'success' | 'failure' | 'aborted';\n\nexport interface GitOperation {\n id?: number;\n operation: GitOperationType;\n branch?: string;\n issueId?: string;\n beforeSha?: string;\n afterSha?: string;\n remoteSha?: string;\n status: GitOperationStatus;\n error?: string;\n ts: string;\n}\n\nexport interface GitOperationFilter {\n issueId?: string;\n operation?: GitOperationType;\n status?: GitOperationStatus;\n since?: string;\n limit?: number;\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport function appendGitOperationSync(op: Omit<GitOperation, 'id'>): number {\n const result = overdeckDb().prepare(`\n INSERT INTO git_operations (\n operation, branch, issue_id,\n before_sha, after_sha, remote_sha,\n status, error, ts\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n `).run(\n op.operation,\n op.branch ?? null,\n op.issueId ?? null,\n op.beforeSha ?? null,\n op.afterSha ?? null,\n op.remoteSha ?? null,\n op.status,\n op.error ?? null,\n new Date(op.ts).getTime(),\n );\n return result.lastInsertRowid as number;\n}\n\nexport function listGitOperationsSync(filter: GitOperationFilter = {}): GitOperation[] {\n const conditions: string[] = [];\n const params: (string | number)[] = [];\n\n if (filter.issueId) { conditions.push('issue_id = ?'); params.push(filter.issueId); }\n if (filter.operation) { conditions.push('operation = ?'); params.push(filter.operation); }\n if (filter.status) { conditions.push('status = ?'); params.push(filter.status); }\n if (filter.since) { conditions.push('ts >= ?'); params.push(new Date(filter.since).getTime()); }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n const limit = filter.limit ? `LIMIT ${filter.limit}` : 'LIMIT 500';\n\n const rows = overdeckDb().prepare(`\n SELECT id, operation, branch, issue_id, before_sha, after_sha, remote_sha, status, error, ts\n FROM git_operations ${where} ORDER BY ts DESC ${limit}\n `).all(...params) as Array<{\n id: number;\n operation: string;\n branch: string | null;\n issue_id: string | null;\n before_sha: string | null;\n after_sha: string | null;\n remote_sha: string | null;\n status: string;\n error: string | null;\n ts: number;\n }>;\n\n return rows.map((row) => ({\n id: row.id,\n operation: row.operation as GitOperationType,\n branch: row.branch ?? undefined,\n issueId: row.issue_id ?? undefined,\n beforeSha: row.before_sha ?? undefined,\n afterSha: row.after_sha ?? undefined,\n remoteSha: row.remote_sha ?? undefined,\n status: row.status as GitOperationStatus,\n error: row.error ?? undefined,\n ts: new Date(row.ts).toISOString(),\n }));\n}\n","/**\n * Git Activity Service (PAN-653)\n *\n * Write path and read path for git operation events persisted to the\n * git_operations SQLite table. Survives dashboard restart — unlike the\n * in-memory activityLog array which is capped at 100 and wiped on restart.\n *\n * All functions use the async-safe getDatabase() which runs under Node 22.\n * NEVER use execSync/readFileSync here — this runs in the dashboard server.\n */\n\nimport { Effect, Data } from 'effect';\nimport {\n appendGitOperationSync,\n listGitOperationsSync,\n} from './overdeck/git-activity.js';\nimport type {\n GitOperation,\n GitOperationFilter,\n GitOperationType,\n GitOperationStatus,\n} from './overdeck/git-activity.js';\n\nexport type { GitOperation, GitOperationFilter, GitOperationType, GitOperationStatus };\nexport { appendGitOperationSync, listGitOperationsSync };\n\n/** A database operation against the git_operations table failed. */\nexport class GitActivityDbError extends Data.TaggedError('GitActivityDbError')<{\n readonly operation: string;\n readonly cause: unknown;\n}> {}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\n/**\n * Effect-native appendGitOperation — typed-error variant of the SQLite write.\n * Fails with GitActivityDbError if the insert throws (e.g. DB locked).\n */\nexport const appendGitOperation = (\n op: Omit<GitOperation, 'id'>,\n): Effect.Effect<number, GitActivityDbError> =>\n Effect.try({\n try: () => appendGitOperationSync(op),\n catch: (cause) => new GitActivityDbError({ operation: 'appendGitOperation', cause }),\n });\n\n/**\n * Effect-native listGitOperations — typed-error variant of the SQLite read.\n * Fails with GitActivityDbError on query failure.\n */\nexport const listGitOperations = (\n filter: GitOperationFilter = {},\n): Effect.Effect<readonly GitOperation[], GitActivityDbError> =>\n Effect.try({\n try: () => listGitOperationsSync(filter),\n catch: (cause) => new GitActivityDbError({ operation: 'listGitOperations', cause }),\n });\n","/**\n * Git Operations Helpers (PAN-653)\n *\n * Thin wrappers over raw git commands that:\n * 1. Use execAsync (never execSync) — safe for dashboard server use\n * 2. Record before/after/remote SHAs for each operation\n * 3. Emit structured git.* events to the git_operations SQLite table\n * 4. For gitPush: perform a pre-push fetch + ancestor check and throw\n * MainDivergedError if origin/main has advanced past the local ancestor\n *\n * Usage: import { gitPush, gitFetch, gitForcePush, gitMerge, gitRevParse }\n * from 'src/lib/git/operations.js'\n *\n * NEVER add execSync, readFileSync, writeFileSync here — this module is\n * imported by dashboard server code (CLAUDE.md rule).\n */\n\nimport { execFile } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect } from 'effect';\nimport { appendGitOperationSync } from '../git-activity.js';\nimport { GitError, VcsError } from '../errors.js';\n\nconst execFileAsync = promisify(execFile);\n\n// ============== Error types ==============\n\n/**\n * Thrown by gitPush when origin/main has advanced beyond the local ancestor.\n * The caller must NOT push — it should call markWorkspaceStuck instead.\n */\nexport class MainDivergedError extends Error {\n readonly localSha: string;\n readonly remoteSha: string;\n\n constructor(localSha: string, remoteSha: string) {\n super(\n `Main diverged: remote HEAD (${remoteSha.slice(0, 7)}) is not an ancestor of local HEAD (${localSha.slice(0, 7)}). Aborting push to protect the hotfix commit.`\n );\n this.name = 'MainDivergedError';\n this.localSha = localSha;\n this.remoteSha = remoteSha;\n }\n}async function gitRevParsePromise(cwd: string, ref: string): Promise<string | null> {\n const ts = new Date().toISOString();\n try {\n const { stdout } = await execFileAsync('git', ['rev-parse', ref], { cwd, encoding: 'utf-8' });\n const sha = stdout.trim();\n appendGitOperationSync({ operation: 'rev_parse', branch: ref, issueId: undefined, afterSha: sha, status: 'success', ts });\n return sha || null;\n } catch {\n return null;\n }\n}async function gitFetchPromise(\n cwd: string,\n remote = 'origin',\n branch?: string,\n opts: { issueId?: string } = {},\n): Promise<void> {\n const ts = new Date().toISOString();\n const args = branch ? ['fetch', remote, branch] : ['fetch', remote];\n try {\n await execFileAsync('git', args, { cwd, encoding: 'utf-8', timeout: 30000 });\n appendGitOperationSync({\n operation: 'fetch',\n branch: branch ?? remote,\n issueId: opts.issueId,\n status: 'success',\n ts,\n });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n appendGitOperationSync({\n operation: 'fetch',\n branch: branch ?? remote,\n issueId: opts.issueId,\n status: 'failure',\n error: msg,\n ts,\n });\n throw err;\n }\n}async function gitPushPromise(\n cwd: string,\n remote = 'origin',\n branch = 'main',\n opts: { issueId?: string } = {},\n): Promise<void> {\n const ts = new Date().toISOString();\n\n // Step 1: record the local HEAD before the push\n const localSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? 'unknown';\n\n // Step 2: fetch latest remote state\n await Effect.runPromise(gitFetch(cwd, remote, branch, opts));\n\n // Step 3: read the remote tracking SHA\n const remoteSha = await Effect.runPromise(gitRevParse(cwd, `${remote}/${branch}`)) ?? '';\n\n // Step 4: ancestor check — is remoteSha an ancestor of localSha?\n if (remoteSha) {\n try {\n // git merge-base --is-ancestor <commit> <commit> exits 0 if true, 1 if false\n await execFileAsync('git', ['merge-base', '--is-ancestor', remoteSha, localSha], {\n cwd,\n encoding: 'utf-8',\n timeout: 10000,\n });\n } catch (err: unknown) {\n // exit code 1 = \"not an ancestor\" (true divergence); any other code is a real git error\n if ((err as { code?: number }).code !== 1) throw err;\n appendGitOperationSync({\n operation: 'main_diverged',\n branch,\n issueId: opts.issueId,\n beforeSha: localSha,\n remoteSha,\n status: 'aborted',\n error: `origin/${branch} (${remoteSha.slice(0, 7)}) is not ancestor of local HEAD (${localSha.slice(0, 7)})`,\n ts,\n });\n throw new MainDivergedError(localSha, remoteSha);\n }\n }\n\n // Step 5: push\n try {\n await execFileAsync('git', ['push', remote, branch], { cwd, encoding: 'utf-8', timeout: 60000 });\n const afterSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? localSha;\n appendGitOperationSync({\n operation: 'push',\n branch,\n issueId: opts.issueId,\n beforeSha: localSha,\n afterSha,\n remoteSha,\n status: 'success',\n ts,\n });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n appendGitOperationSync({\n operation: 'push',\n branch,\n issueId: opts.issueId,\n beforeSha: localSha,\n remoteSha,\n status: 'failure',\n error: msg,\n ts,\n });\n throw err;\n }\n}async function gitForcePushPromise(\n cwd: string,\n remote = 'origin',\n branch = 'main',\n opts: { issueId?: string; reason?: string } = {},\n): Promise<void> {\n const ts = new Date().toISOString();\n const localSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? 'unknown';\n const remoteSha = await Effect.runPromise(gitRevParse(cwd, `${remote}/${branch}`)) ?? undefined;\n\n try {\n await execFileAsync('git', ['push', '--force-with-lease', remote, branch], {\n cwd,\n encoding: 'utf-8',\n timeout: 60000,\n });\n const afterSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? localSha;\n appendGitOperationSync({\n operation: 'force_push',\n branch,\n issueId: opts.issueId,\n beforeSha: localSha,\n afterSha,\n remoteSha,\n status: 'success',\n error: opts.reason,\n ts,\n });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n appendGitOperationSync({\n operation: 'force_push',\n branch,\n issueId: opts.issueId,\n beforeSha: localSha,\n remoteSha,\n status: 'failure',\n error: msg,\n ts,\n });\n throw err;\n }\n}async function gitMergePromise(\n cwd: string,\n branch: string,\n opts: { issueId?: string; noFf?: boolean } = {},\n): Promise<void> {\n const ts = new Date().toISOString();\n const beforeSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? 'unknown';\n const args = opts.noFf ? ['merge', '--no-ff', branch] : ['merge', branch];\n try {\n await execFileAsync('git', args, { cwd, encoding: 'utf-8', timeout: 60000 });\n const afterSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? beforeSha;\n appendGitOperationSync({\n operation: 'merge',\n branch,\n issueId: opts.issueId,\n beforeSha,\n afterSha,\n status: 'success',\n ts,\n });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n appendGitOperationSync({\n operation: 'merge',\n branch,\n issueId: opts.issueId,\n beforeSha,\n status: 'failure',\n error: msg,\n ts,\n });\n throw err;\n }\n}\n\n// ─── Effect variants (PAN-1249, additive) ────────────────────────────────────\n//\n// These wrap the existing Promise-based functions with typed Effect error\n// channels (GitError / VcsError). The underlying impl is unchanged; this is\n// an additive surface for callers that already speak Effect. MainDivergedError\n// is thrown by the underlying gitPush; the Effect variant surfaces it\n// preserved as the cause inside a VcsError tagged 'main-diverged'.\n\n/** Resolve a git ref to its SHA. Returns null if the ref does not exist. */\nexport function gitRevParse(\n cwd: string,\n ref: string,\n): Effect.Effect<string | null> {\n return Effect.promise(() => gitRevParsePromise(cwd, ref));\n}\n\n/** Fetch from a remote. */\nexport function gitFetch(\n cwd: string,\n remote = 'origin',\n branch?: string,\n opts: { issueId?: string } = {},\n): Effect.Effect<void, GitError> {\n return Effect.tryPromise({\n try: () => gitFetchPromise(cwd, remote, branch, opts),\n catch: (cause) =>\n new GitError({\n command: branch ? ['git', 'fetch', remote, branch] : ['git', 'fetch', remote],\n stderr: cause instanceof Error ? cause.message : String(cause),\n exitCode: -1,\n cause,\n }),\n });\n}\n\n/**\n * Push a branch to a remote with divergence guard.\n * Fails with VcsError tagged 'main-diverged' when MainDivergedError is thrown.\n */\nexport function gitPush(\n cwd: string,\n remote = 'origin',\n branch = 'main',\n opts: { issueId?: string } = {},\n): Effect.Effect<void, VcsError | GitError> {\n return Effect.tryPromise({\n try: () => gitPushPromise(cwd, remote, branch, opts),\n catch: (cause) => {\n if (cause instanceof MainDivergedError) {\n return new VcsError({\n operation: 'main-diverged',\n message: cause.message,\n cause,\n });\n }\n return new GitError({\n command: ['git', 'push', remote, branch],\n stderr: cause instanceof Error ? cause.message : String(cause),\n exitCode: -1,\n cause,\n });\n },\n });\n}\n\n/** Force-push a branch (--force-with-lease). */\nexport function gitForcePush(\n cwd: string,\n remote = 'origin',\n branch = 'main',\n opts: { issueId?: string; reason?: string } = {},\n): Effect.Effect<void, GitError> {\n return Effect.tryPromise({\n try: () => gitForcePushPromise(cwd, remote, branch, opts),\n catch: (cause) =>\n new GitError({\n command: ['git', 'push', '--force-with-lease', remote, branch],\n stderr: cause instanceof Error ? cause.message : String(cause),\n exitCode: -1,\n cause,\n }),\n });\n}\n\n/** Merge a branch into the current branch. */\nexport function gitMerge(\n cwd: string,\n branch: string,\n opts: { issueId?: string; noFf?: boolean } = {},\n): Effect.Effect<void, GitError> {\n return Effect.tryPromise({\n try: () => gitMergePromise(cwd, branch, opts),\n catch: (cause) =>\n new GitError({\n command: opts.noFf ? ['git', 'merge', '--no-ff', branch] : ['git', 'merge', branch],\n stderr: cause instanceof Error ? cause.message : String(cause),\n exitCode: -1,\n cause,\n }),\n });\n}\n","/**\n * Merge Agent - Automatic merge conflict resolution using Claude Code\n */\n\nimport { existsSync, mkdirSync, appendFileSync } from 'fs';\nimport { writeFile } from 'fs/promises';\nimport { join, dirname, basename, relative } from 'path';\nimport { fileURLToPath } from 'url';\nimport { spawn, exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect } from 'effect';\nimport { capturePane, killSession, listSessionNames, sendKeys, sessionExists } from '../tmux.js';\nimport { emitActivityEntrySync, emitActivityTtsSync } from '../activity-logger.js';\nimport { loadConfigSync } from '../config-yaml.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Paths that must never enter a pipeline auto-commit, regardless of gitignore\n * state. These are workspace-local or machine-local state files and sync-target\n * directories; committing them pollutes feature branches and main.\n */\nexport const AUTO_COMMIT_EXCLUDED_PATHS = [\n '.pan/kickoff.md',\n '.pan/continue.json',\n '.pan/handoff-*.md',\n '.pan/spec.vbrief.json',\n '.claude/rules/',\n '.claude/skills/',\n // PAN-1899: machine-local Overdeck config copied into every workspace by\n // copyOverdeckSettingsToWorkspaceSync (config.yaml, projects.yaml,\n // settings.json). It diverges from main constantly and must never enter a\n // feature branch — that was the source of the recurring projects.yaml sync\n // conflict.\n '.overdeck/',\n];\n\nconst SYNC_MAIN_MAIN_PREFERRED_PATHS = [\n '.pan/continues',\n '.pan/specs',\n '.beads',\n];\n\nexport function isSyncMainMainPreferredPath(relativePath: string): boolean {\n const normalized = relativePath.replace(/\\\\/g, '/');\n return SYNC_MAIN_MAIN_PREFERRED_PATHS.some(\n (prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`),\n );\n}\n\nexport function isAutoCommitExcludedPath(relativePath: string): boolean {\n const normalized = relativePath.replace(/\\\\/g, '/');\n for (const pattern of AUTO_COMMIT_EXCLUDED_PATHS) {\n if (pattern.endsWith('/')) {\n if (normalized.startsWith(pattern) || normalized === pattern.slice(0, -1)) {\n return true;\n }\n } else if (pattern.includes('*')) {\n const regex = new RegExp(\n '^' + pattern.replace(/\\./g, '\\\\.').replace(/\\*/g, '[^/]*') + '$'\n );\n if (regex.test(normalized)) return true;\n } else if (normalized === pattern) {\n return true;\n }\n }\n return false;\n}\n\nfunction parseStatusPath(line: string): string {\n // git status --porcelain lines are \"XY PATH\" or \"XY ORIG -> DEST\" for renames.\n const body = line.slice(3);\n if (line[0] === 'R' || line[1] === 'R') {\n const parts = body.split(' -> ');\n return parts[parts.length - 1];\n }\n return body;\n}\n\n/**\n * Auto-commit non-excluded workspace changes before a sync-main merge.\n * Respects .gitignore (no -f), unstages excluded paths, and leaves excluded\n * paths dirty so the sync can proceed. Returns success=false on git errors.\n */\nexport async function autoCommitWorkspaceChangesBeforeSync(\n projectPath: string,\n): Promise<{ success: boolean; committed: boolean; reason?: string }> {\n try {\n const { stdout: statusOut } = await execAsync('git status --porcelain', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n if (!statusOut.trim()) {\n return { success: true, committed: false, reason: 'no uncommitted changes' };\n }\n\n // PAN-1819: plain `git add -A` respects .gitignore; never use -f.\n await execAsync('git add -A', { cwd: projectPath, encoding: 'utf-8' });\n\n // Belt-and-suspenders: unstage excluded paths regardless of ignore state.\n const resetPaths = AUTO_COMMIT_EXCLUDED_PATHS.map((p) =>\n p.endsWith('/') ? p.slice(0, -1) : p\n ).join(' ');\n await execAsync(`git reset HEAD -- ${resetPaths}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n\n const { stdout: diffStat } = await execAsync('git diff --cached --stat', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n if (!diffStat.trim()) {\n return { success: true, committed: false, reason: 'only excluded/ignored changes remain' };\n }\n\n await execAsync('git commit -m \"chore: auto-commit before sync with main\"', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n return { success: true, committed: true };\n } catch (error: any) {\n return { success: false, committed: false, reason: `Failed to auto-commit: ${error.message}` };\n }\n}\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\nimport {\n OVERDECK_HOME,\n} from '../paths.js';\nimport { resolveGitHubIssueSync } from '../tracker-utils.js';\n\nimport { restoreTrackedBeadsExport } from '../beads-restore.js';\nimport { runQualityGates } from './validation.js';\nimport { loadProjectsConfigSync } from '../projects.js';\nimport { cleanupStaleLocks } from '../git-utils.js';\nimport { gitPush, MainDivergedError } from '../git/operations.js';\nimport { markWorkspaceStuck, setReviewStatusSync } from '../review-status.js';\nimport { appendGitOperationSync, type GitOperationType } from '../git-activity.js';\nimport { recordFeatureRegistryLifecycle } from '../registry/feature-registry-population.js';\nimport { verifyMergedBeforeLifecycle, type PostMergeLifecycleOptions } from './merge-verification.js';\n\nconst SPECIALISTS_DIR = join(OVERDECK_HOME, 'specialists');\nconst MERGE_HISTORY_DIR = join(SPECIALISTS_DIR, 'merge-agent');\nconst MERGE_HISTORY_FILE = join(MERGE_HISTORY_DIR, 'history.jsonl');\n\n/**\n * Context for a merge conflict resolution request\n */\nexport interface MergeConflictContext {\n projectPath: string;\n sourceBranch: string;\n targetBranch: string;\n conflictFiles: string[];\n issueId: string;\n testCommand?: string;\n}\n\n/**\n * Result of merge agent execution\n */\nexport interface MergeResult {\n success: boolean;\n resolvedFiles?: string[];\n failedFiles?: string[];\n testsStatus?: 'PASS' | 'FAIL' | 'SKIP';\n validationStatus?: 'PASS' | 'FAIL' | 'NOT_RUN';\n reason?: string;\n notes?: string;\n output?: string;\n}\n\n/**\n * Merge history entry\n */\ninterface MergeHistoryEntry {\n timestamp: string;\n issueId: string;\n sourceBranch: string;\n targetBranch: string;\n conflictFiles: string[];\n result: MergeResult;\n sessionId?: string;\n}\n\n/**\n * Timeout for merge agent in milliseconds (15 minutes)\n */\n/**\n * Notify TLDR daemon to reindex changed files after merge\n */\nexport async function notifyTldrDaemon(projectPath: string, _sourceBranch: string): Promise<void> {\n try {\n console.log(`[merge-agent] Notifying TLDR daemon to reindex changed files...`);\n\n // Check if TLDR daemon is available\n const venvPath = join(projectPath, '.venv');\n if (!existsSync(venvPath)) {\n console.log(`[merge-agent] No .venv found, skipping TLDR notification`);\n return;\n }\n\n // Get changed files from the merge\n const { stdout } = await execAsync(`git diff --name-only HEAD~1 HEAD`, {\n cwd: projectPath,\n encoding: 'utf-8'\n });\n\n const changedFiles = stdout\n .trim()\n .split('\\n')\n .filter(f => f.trim().length > 0)\n .filter(f => {\n // Only include source code files (skip docs, configs, etc)\n const ext = f.split('.').pop()?.toLowerCase();\n return ext && ['ts', 'js', 'tsx', 'jsx', 'py', 'java', 'go', 'rs', 'cpp', 'c', 'h'].includes(ext);\n });\n\n if (changedFiles.length === 0) {\n console.log(`[merge-agent] No source files changed, skipping TLDR notification`);\n return;\n }\n\n console.log(`[merge-agent] Found ${changedFiles.length} changed source files to reindex`);\n\n // Get TLDR daemon service\n const { getTldrDaemonServiceSync } = await import('../tldr-daemon.js');\n const tldrService = getTldrDaemonServiceSync(projectPath, venvPath);\n\n // Check if daemon is running\n const status = await tldrService.getStatus();\n if (!status.running) {\n console.log(`[merge-agent] TLDR daemon not running, skipping notification`);\n return;\n }\n\n // Trigger warm to reindex (this will update the index incrementally)\n console.log(`[merge-agent] Triggering TLDR index warm...`);\n await tldrService.warm(true); // background mode\n\n console.log(`[merge-agent] ✓ TLDR daemon notified to reindex`);\n logActivity('tldr_notified', `Notified TLDR daemon to reindex ${changedFiles.length} files`);\n } catch (error: any) {\n // Non-fatal - log warning and continue\n console.warn(`[merge-agent] Failed to notify TLDR daemon: ${error.message}`);\n logActivity('tldr_notify_error', `TLDR notification failed: ${error.message}`);\n }\n}\n\n/**\n * Post-merge handoff: mark merged work as verifying on main and free runtime resources.\n *\n * Leaves the issue, workspace, vBRIEF, branches, and agent state dirs intact.\n * The explicit close-out ceremony performs final archival and destructive cleanup.\n *\n * IDEMPOTENT: Safe to call multiple times for the same issueId. Tracks completed\n * issues and returns immediately on re-entry. This is defense-in-depth against\n * the infinite loop that burned 24,626 Linear API calls (PAN-328).\n */\n\n// Defense-in-depth: track issues that have completed postMergeLifecycle.\n// Prevents re-execution even if caller guards fail. Persists for server lifetime.\nconst _completedPostMerge = new Set<string>();\nconst _postMergeInFlight = new Map<string, Promise<void>>();\n\n// PAN-1531: dropLingeringPreMergeStashes removed. The pre-merge stash kind\n// is no longer created by Overdeck, so there's nothing for the post-merge\n// lifecycle to clean up. Pre-existing pre-merge:* residue in refs/stash is\n// expected and inert.\n\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\"'\"'`)}'`;\n}\n\nexport async function postMergeLifecycle(\n issueId: string,\n projectPath: string,\n sourceBranch?: string,\n options?: PostMergeLifecycleOptions,\n): Promise<void> {\n // PAN-1517: the per-slot swarm runtime is gone. Slot branches no longer exist\n // — parallelism is an in-context concern owned by the work agent (see\n // roles/work.md \"Parallel work via subagents\"). postMergeLifecycle fires only\n // for the issue's main feature branch merging to `main`.\n\n // Guard 1: skip if already completed (defense-in-depth against infinite loops)\n if (_completedPostMerge.has(issueId)) {\n console.log(`[merge-agent] postMergeLifecycle already completed for ${issueId}, skipping`);\n return;\n }\n\n const inFlight = _postMergeInFlight.get(issueId);\n if (inFlight) {\n console.log(`[merge-agent] postMergeLifecycle already running for ${issueId}, joining in-flight run`);\n return inFlight;\n }\n\n const run = (async () => {\n // Guard 2: closed-out is TERMINAL. Close-out flips the spec on main to\n // completed/cancelled, clears review status, and closes the tracker issue.\n // Re-running the handoff after that resurrects the review row and REOPENS\n // the closed issue — observed live on PAN-1190 (2026-06-11): the deacon's\n // stale-mergeStatus sweep saw the cleared row as \"stale\" 47 minutes after\n // close-out and the handoff reopened it into verifying-on-main forever.\n try {\n const { findSpecByIssue } = await import('../pan-dir/specs.js');\n const spec = await Effect.runPromise(findSpecByIssue(projectPath, issueId));\n if (spec && (spec.status === 'completed' || spec.status === 'cancelled')) {\n console.log(`[merge-agent] ${issueId} is closed out (spec ${spec.status}) — skipping post-merge lifecycle`);\n _completedPostMerge.add(issueId);\n return;\n }\n } catch {\n // Spec unreadable — proceed; the guard is best-effort.\n }\n\n const mergeVerification = await verifyMergedBeforeLifecycle(issueId, projectPath, sourceBranch, options);\n if (!mergeVerification.merged) {\n console.warn(`[merge-agent] Refusing post-merge lifecycle for ${issueId}: ${mergeVerification.reason}`);\n return;\n }\n console.log(`[merge-agent] Verified merge before lifecycle for ${issueId}: ${mergeVerification.reason}`);\n\n // Set mergeStatus='merged' after verifying the branch or PR actually landed.\n try {\n setReviewStatusSync(issueId, {\n mergeStatus: 'merged',\n readyForMerge: false,\n ...(options?.markReviewPassed ? { reviewStatus: 'passed' as const } : {}),\n });\n console.log(`[merge-agent] ✓ mergeStatus set to 'merged' for ${issueId}`);\n } catch (err: any) {\n console.warn(`[merge-agent] Could not set mergeStatus: ${err.message}`);\n }\n\n // Step 0: Write pending lifecycle file and spawn detached deploy script.\n // The deploy script rebuilds dist/, kills this server, and starts a fresh process.\n // The fresh process reads the pending file on startup and runs the lifecycle steps\n // with correct module chunk references (no ERR_MODULE_NOT_FOUND after merge).\n //\n // Skip this step when we ARE the fresh process (called from processPendingLifecycle) —\n // dynamic imports already resolve correctly and spawning again would create an infinite loop.\n if (!options?.skipDeploy) {\n const pendingFile = join(OVERDECK_HOME, 'pending-post-merge.json');\n let repoRoot = __dirname.includes('/src/')\n ? __dirname.replace(/\\/src\\/.*$/, '')\n : __dirname.replace(/\\/dist\\/.*$/, '').replace(/\\/lib\\/.*$/, '');\n // If running from a workspace (workspaces/feature-*/), resolve to the main repo root.\n // Without this, the deploy script builds and npm-links from the workspace, hijacking\n // the global `pan` CLI to point at stale workspace code.\n const wsMatch = repoRoot.match(/^(.+)\\/workspaces\\/feature-[^/]+$/);\n if (wsMatch) {\n repoRoot = wsMatch[1];\n console.log(`[merge-agent] Resolved workspace repoRoot to main repo: ${repoRoot}`);\n }\n const deployScript = join(repoRoot, 'scripts', 'post-merge-deploy.sh');\n\n try {\n const pendingData = JSON.stringify({\n issueId,\n projectPath,\n sourceBranch: sourceBranch ?? '',\n timestamp: Date.now(),\n reason: 'post-merge',\n trigger: 'merge-agent',\n });\n await writeFile(pendingFile, pendingData, 'utf-8');\n console.log(`[merge-agent] Wrote pending lifecycle file: ${pendingFile}`);\n\n // Pass 'post-merge' as the reason to the deploy script so it writes the\n // restart marker. We spawn detached and return immediately — the deploy script\n // kills this server. The new server reads the pending file on boot,\n // emits lifecycle_started, and after processing emits lifecycle_complete/failed.\n const child = spawn(deployScript, [repoRoot, issueId, projectPath, sourceBranch ?? '', 'post-merge'], {\n detached: true,\n stdio: 'ignore',\n });\n child.unref();\n console.log(`[merge-agent] Spawned detached deploy script (pid ${child.pid}) — server will restart with new build`);\n return;\n } catch (err: any) {\n console.warn(`[merge-agent] Failed to spawn deploy script: ${err.message}. Falling through to in-process lifecycle (may fail on stale chunks).`);\n }\n }\n\n console.log(`[merge-agent] Running post-merge verify handoff for ${issueId}`);\n\n // 1. Clean up stale workflow labels and keep the legacy merged marker for history.\n // verifying-on-main is applied next and takes precedence in canonical state mapping.\n try {\n const { cleanupMergedLabels } = await import('../lifecycle/label-cleanup.js');\n const ghResolved = resolveGitHubIssueSync(issueId);\n const labelCtx = ghResolved.isGitHub\n ? { issueId, projectPath, github: { owner: ghResolved.owner, repo: ghResolved.repo, number: ghResolved.number } }\n : { issueId, projectPath };\n // PAN-1249: cleanupMergedLabels returns Effect<StepResult>; bridge to Promise.\n const labelResult = await Effect.runPromise(cleanupMergedLabels(labelCtx));\n if (labelResult.success && !labelResult.skipped) {\n console.log(`[merge-agent] ✓ ${labelResult.details?.join('; ')}`);\n logActivity('labels_cleaned', labelResult.details?.join('; ') || 'Labels cleaned');\n } else if (labelResult.skipped) {\n console.log(`[merge-agent] Label cleanup skipped: ${labelResult.details?.join('; ')}`);\n } else {\n console.warn(`[merge-agent] Label cleanup failed (non-fatal): ${labelResult.error}`);\n }\n } catch (err) {\n console.warn(`[merge-agent] Could not clean labels: ${err}`);\n }\n\n try {\n await transitionIssueToVerifyingOnMain(issueId, projectPath);\n void recordFeatureRegistryLifecycle({ issueId, status: 'merged' });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.warn(`[merge-agent] Could not transition issue to verifying_on_main: ${message}`);\n try {\n setReviewStatusSync(issueId, {\n mergeStatus: 'failed',\n readyForMerge: false,\n mergeNotes: `Post-merge verifying_on_main transition failed: ${message}`,\n });\n } catch (statusErr: any) {\n console.warn(`[merge-agent] Could not persist verifying_on_main transition failure: ${statusErr?.message ?? statusErr}`);\n }\n announceMerge('failed', issueId, `Post-merge verifying_on_main transition failed: ${message}`);\n logActivity('merge_failed', `Post-merge verifying_on_main transition failed for ${issueId}: ${message}`);\n throw err;\n }\n\n // 2. Compact old beads (via lifecycle module)\n try {\n const { compactBeads } = await import('../lifecycle/compact-beads.js');\n // PAN-1249: compactBeads returns Effect<StepResult>; bridge to Promise.\n const beadsResult = await Effect.runPromise(compactBeads({ issueId, projectPath }));\n if (beadsResult.success && !beadsResult.skipped) {\n console.log(`[merge-agent] ✓ ${beadsResult.details?.join('; ')}`);\n logActivity('beads_compaction_complete', beadsResult.details?.join('; ') || 'Beads compacted');\n }\n } catch (err) {\n console.warn(`[merge-agent] Beads compaction failed: ${err}`);\n }\n\n // 3. Pause work/planning agents and kill their tmux panes to free resources.\n try {\n const { setAgentPaused, getAgentState } = await import('../agents.js');\n const { killSession, sessionExists } = await import('../tmux.js');\n const issueLower = issueId.toLowerCase();\n const reason = 'awaiting close-out (verify on main)';\n for (const agentId of [`agent-${issueLower}`, `planning-${issueLower}`]) {\n // Pause, then VERIFY the gate actually persisted to state.json. A server\n // restart mid-lifecycle (the PAN-1723 deploy re-runs this from\n // pending-post-merge.json) or a concurrent deacon read-modify-write on\n // state.json can silently drop the pause. An unpaused merged work agent\n // sits idle at its prompt yet still counts against the PAN-1665 work\n // ceiling, throttling dispatch for every live issue (PAN-1726). Assert\n // paused=true after the write; retry once, then fail loudly.\n const initial = await Effect.runPromise(setAgentPaused(agentId, reason, true));\n if (initial === null) {\n // No state.json for this agent — nothing to pause (e.g. planning never ran).\n continue;\n }\n let verify = await Effect.runPromise(getAgentState(agentId));\n if (verify?.paused !== true) {\n await Effect.runPromise(setAgentPaused(agentId, reason, true));\n verify = await Effect.runPromise(getAgentState(agentId));\n }\n if (verify?.paused === true) {\n console.log(`[merge-agent] ✓ Paused ${agentId}: ${reason}`);\n } else {\n console.error(\n `[merge-agent] ✗ FAILED to persist pause for ${agentId} after merge — state.json paused=${verify?.paused}. ` +\n `Idle merged work agent may hold a PAN-1665 work slot and throttle dispatch (PAN-1726).`,\n );\n logActivity('agent_pause_failed', `Could not persist pause for ${agentId} after merge — may throttle dispatch (PAN-1726)`);\n }\n if (await Effect.runPromise(sessionExists(agentId))) {\n await Effect.runPromise(killSession(agentId));\n console.log(`[merge-agent] ✓ Killed ${agentId} tmux session to free resources`);\n logActivity('agent_session_killed', `Freed resources: killed tmux session for ${agentId}`);\n }\n }\n } catch (err) {\n console.warn(`[merge-agent] Could not pause or kill agent sessions: ${err}`);\n }\n\n // 5a. Kill canonical reviewer/synthesis sessions (PAN-915).\n // Sessions persist across review rounds to preserve reviewer context, so the\n // merge is the right moment to tear them down. Issue is done — context value\n // is zero, RSS leak risk is non-zero. Resolve projectKey from the project\n // path so we don't depend on caller-supplied config.\n try {\n const { killAllReviewerSessions } = await import('./review-agent.js');\n const { resolveProjectFromIssueSync } = await import('../projects.js');\n const resolved = resolveProjectFromIssueSync(issueId);\n const projectKey = resolved?.projectKey;\n if (projectKey) {\n const { killed } = await Effect.runPromise(killAllReviewerSessions(projectKey, issueId));\n if (killed.length > 0) {\n console.log(`[merge-agent] ✓ Killed ${killed.length} canonical reviewer session(s) for ${issueId}`);\n logActivity('reviewer_sessions_killed', `Killed ${killed.length} reviewer session(s) for ${issueId} on merge`);\n }\n }\n } catch (err) {\n console.warn(`[merge-agent] Could not kill canonical reviewer sessions: ${err}`);\n }\n\n await killPostMergeRoleSessions(issueId);\n\n // 3c. Create a workspace-scoped memory reset marker so old review-blocker noise stays archived but out of retrieval.\n try {\n const { createResetMarker } = await import('../memory/cli.js');\n const timestamp = new Date().toISOString();\n const marker = await createResetMarker({\n projectId: basename(projectPath),\n scope: 'workspace',\n scopeId: `feature-${issueId.toLowerCase()}`,\n reason: 'post-merge cleanup',\n fromTimestamp: timestamp,\n createdAt: timestamp,\n });\n console.log(`[merge-agent] ✓ Created memory reset marker ${marker.id} for ${marker.scope}:${marker.scopeId}`);\n } catch (err) {\n console.warn(`[merge-agent] Memory reset marker creation failed (non-fatal): ${err}`);\n }\n\n // 4. Stop Docker containers + networks to prevent network pool exhaustion (non-fatal)\n // Orphaned Docker networks accumulate when workspaces are merged but containers are never\n // torn down, eventually exhausting Docker's address pool and blocking new workspace creation.\n try {\n const { findWorkspacePath } = await import('../lifecycle/archive-planning.js');\n const { stopWorkspaceDocker } = await import('../workspace-manager.js');\n const issueLower = issueId.toLowerCase();\n const workspacePath = findWorkspacePath(projectPath, issueLower);\n if (workspacePath) {\n const dockerResult = await Effect.runPromise(stopWorkspaceDocker(workspacePath, issueLower));\n if (dockerResult.containersFound) {\n console.log(`[merge-agent] ✓ Stopped Docker containers: ${dockerResult.steps.join('; ')}`);\n logActivity('docker_cleanup', `Stopped Docker for ${issueId}: ${dockerResult.steps.join('; ')}`);\n }\n }\n } catch (err) {\n console.warn(`[merge-agent] Docker cleanup failed (non-fatal): ${err}`);\n }\n\n await notifyTldrDaemon(projectPath, sourceBranch ?? '');\n await maybeSpawnPostMergeKnowledgeRetro(issueId, projectPath);\n\n // Mark completed BEFORE logging — prevents re-entry even if the log line triggers something\n _completedPostMerge.add(issueId);\n\n console.log(`[merge-agent] Post-merge handoff completed for ${issueId}. Awaiting close-out (verify on main).`);\n announceMerge('completed', issueId);\n logActivity('merge_complete', `Merged ${issueId}. Awaiting close-out (verify on main).`);\n })().finally(() => {\n _postMergeInFlight.delete(issueId);\n });\n _postMergeInFlight.set(issueId, run);\n return run;\n}\n\nasync function transitionIssueToVerifyingOnMain(issueId: string, projectPath: string): Promise<void> {\n const [effectModule, issueLifecycleModule, githubModule, linearModule, rallyModule, errorsModule] = await Promise.all([\n import('effect'),\n import('../../dashboard/server/services/issue-lifecycle.js'),\n import('../../dashboard/server/services/github-client.js'),\n import('../../dashboard/server/services/linear-client.js'),\n import('../../dashboard/server/services/rally-client.js'),\n import('../../dashboard/server/services/typed-errors.js'),\n ]);\n const { Effect, Layer } = effectModule;\n const { IssueLifecycle, IssueLifecycleLive } = issueLifecycleModule;\n const { GitHubClient } = githubModule;\n const { LinearClientOptionalLive } = linearModule;\n const { RallyClientOptionalLive } = rallyModule;\n const { IssueNotFound } = errorsModule;\n const gitHubRepo = (owner: string, repo: string) => shellQuote(`${owner}/${repo}`);\n const githubLayer = Layer.succeed(GitHubClient, {\n getIssue: (_owner: string, _repo: string, number: number) => Effect.fail(new IssueNotFound({ id: String(number) })),\n closeIssue: (owner: string, repo: string, number: number) => Effect.promise(() => execAsync(`gh issue close ${number} --repo ${gitHubRepo(owner, repo)}`, { cwd: projectPath, encoding: 'utf-8' }).then(() => undefined)),\n reopenIssue: (owner: string, repo: string, number: number) => Effect.promise(() => execAsync(`gh issue reopen ${number} --repo ${gitHubRepo(owner, repo)} 2>/dev/null || true`, { cwd: projectPath, encoding: 'utf-8' }).then(() => undefined)),\n addLabel: (owner: string, repo: string, number: number, label: string) => Effect.promise(async () => {\n if (label === 'verifying-on-main') {\n await execAsync(`gh label create ${shellQuote(label)} --repo ${gitHubRepo(owner, repo)} --color \"fbca04\" --description \"Merged — awaiting verification on main\" --force 2>/dev/null || true`, { cwd: projectPath, encoding: 'utf-8' });\n }\n await execAsync(`gh issue edit ${number} --repo ${gitHubRepo(owner, repo)} --add-label ${shellQuote(label)}`, { cwd: projectPath, encoding: 'utf-8' });\n }),\n removeLabel: (owner: string, repo: string, number: number, label: string) => Effect.promise(() => execAsync(`gh issue edit ${number} --repo ${gitHubRepo(owner, repo)} --remove-label ${shellQuote(label)} 2>/dev/null || true`, { cwd: projectPath, encoding: 'utf-8' }).then(() => undefined)),\n ensureLabel: (owner: string, repo: string, label: string, color = '0075ca', description = '') => Effect.promise(async () => {\n await execAsync(`gh label create ${shellQuote(label)} --repo ${gitHubRepo(owner, repo)} --color ${shellQuote(color)} --description ${shellQuote(description)} --force 2>/dev/null || true`, { cwd: projectPath, encoding: 'utf-8' });\n return { id: 0, name: label, color };\n }),\n addComment: () => Effect.void,\n getComments: () => Effect.succeed([]),\n });\n const layer = IssueLifecycleLive.pipe(\n Layer.provide(LinearClientOptionalLive),\n Layer.provide(githubLayer),\n Layer.provide(RallyClientOptionalLive),\n );\n await Effect.runPromise(\n Effect.gen(function* () {\n const lifecycle = yield* IssueLifecycle;\n yield* lifecycle.transitionTo(issueId, 'verifying_on_main');\n }).pipe(Effect.provide(layer)),\n );\n console.log(`[merge-agent] ✓ Transitioned ${issueId} to verifying_on_main`);\n}\n\nfunction isPostMergeRoleSession(sessionName: string, issueLower: string): boolean {\n if ([`agent-${issueLower}-test`, `agent-${issueLower}-ship`, `agent-${issueLower}-merge`].includes(sessionName)) {\n return true;\n }\n if (sessionName.startsWith(`agent-${issueLower}-review-`)) {\n return true;\n }\n return sessionName.startsWith('specialist-')\n && sessionName.includes(`-${issueLower}-`)\n && /-(review|test|merge|ship)(?:-|$)/.test(sessionName);\n}\n\nasync function killPostMergeRoleSessions(issueId: string): Promise<void> {\n try {\n const issueLower = issueId.toLowerCase();\n const sessions = await Effect.runPromise(listSessionNames());\n const targets = sessions.filter((session) => isPostMergeRoleSession(session, issueLower));\n for (const session of targets) {\n await Effect.runPromise(killSession(session));\n }\n if (targets.length > 0) {\n console.log(`[merge-agent] ✓ Killed ${targets.length} review/test/ship session(s) for ${issueId}`);\n logActivity('role_sessions_killed', `Killed ${targets.length} review/test/ship session(s) for ${issueId} on merge`);\n }\n } catch (err) {\n console.warn(`[merge-agent] Could not kill role sessions for ${issueId}: ${err}`);\n }\n}\n\nfunction isPostMergeKnowledgeRetroEnabled(): boolean {\n try {\n return loadConfigSync().config.knowledge?.postMergeAutoRetro === true;\n } catch (err) {\n console.warn(`[merge-agent] Could not read knowledge post-merge retro config: ${err}`);\n return false;\n }\n}\n\nasync function maybeSpawnPostMergeKnowledgeRetro(issueId: string, projectPath: string): Promise<void> {\n if (!isPostMergeKnowledgeRetroEnabled()) return;\n\n try {\n const child = spawn('pan', ['knowledge', issueId, '--retro'], {\n cwd: projectPath,\n detached: true,\n stdio: 'ignore',\n });\n child.once?.('error', (err) => {\n const message = err instanceof Error ? err.message : String(err);\n console.warn(`[merge-agent] Post-merge knowledge retro spawn failed for ${issueId}: ${message}`);\n logActivity('knowledge_retro_error', `Post-merge knowledge retro spawn failed for ${issueId}: ${message}`, issueId);\n });\n child.unref();\n console.log(`[merge-agent] Spawned post-merge knowledge retro for ${issueId} (pid ${child.pid})`);\n logActivity('knowledge_retro_spawned', `Spawned post-merge knowledge retro for ${issueId}`, issueId);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.warn(`[merge-agent] Post-merge knowledge retro spawn failed for ${issueId}: ${message}`);\n logActivity('knowledge_retro_error', `Post-merge knowledge retro spawn failed for ${issueId}: ${message}`, issueId);\n }\n}\n\n/**\n * Reset postMergeLifecycle completion tracking for an issue (used by reopen).\n */\nexport function resetPostMergeState(issueId: string): void {\n _completedPostMerge.delete(issueId);\n _postMergeInFlight.delete(issueId);\n}\n\n/**\n * Parse result markers from agent output\n */\nexport function parseAgentOutput(output: string): MergeResult {\n const lines = output.split('\\n');\n\n let mergeResult: 'SUCCESS' | 'FAILURE' | null = null;\n let resolvedFiles: string[] = [];\n let failedFiles: string[] = [];\n let testsStatus: 'PASS' | 'FAIL' | 'SKIP' | null = null;\n let validationStatus: 'PASS' | 'FAIL' | null = null;\n let reason = '';\n let notes = '';\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Match MERGE_RESULT\n if (trimmed.startsWith('MERGE_RESULT:')) {\n const value = trimmed.substring('MERGE_RESULT:'.length).trim();\n if (value === 'SUCCESS' || value === 'FAILURE') {\n mergeResult = value;\n }\n }\n\n // Match RESOLVED_FILES\n if (trimmed.startsWith('RESOLVED_FILES:')) {\n const value = trimmed.substring('RESOLVED_FILES:'.length).trim();\n resolvedFiles = value\n .split(',')\n .map((f) => f.trim())\n .filter((f) => f.length > 0);\n }\n\n // Match FAILED_FILES\n if (trimmed.startsWith('FAILED_FILES:')) {\n const value = trimmed.substring('FAILED_FILES:'.length).trim();\n failedFiles = value\n .split(',')\n .map((f) => f.trim())\n .filter((f) => f.length > 0);\n }\n\n // Match TESTS\n if (trimmed.startsWith('TESTS:')) {\n const value = trimmed.substring('TESTS:'.length).trim();\n if (value === 'PASS' || value === 'FAIL' || value === 'SKIP') {\n testsStatus = value;\n }\n }\n\n // Match VALIDATION\n if (trimmed.startsWith('VALIDATION:')) {\n const value = trimmed.substring('VALIDATION:'.length).trim();\n if (value === 'PASS' || value === 'FAIL') {\n validationStatus = value;\n }\n }\n\n // Match REASON\n if (trimmed.startsWith('REASON:')) {\n reason = trimmed.substring('REASON:'.length).trim();\n }\n\n // Match NOTES\n if (trimmed.startsWith('NOTES:')) {\n notes = trimmed.substring('NOTES:'.length).trim();\n }\n }\n\n // Build result\n if (mergeResult === 'SUCCESS') {\n return {\n success: true,\n resolvedFiles,\n testsStatus: testsStatus || 'SKIP',\n validationStatus: validationStatus || 'NOT_RUN',\n notes,\n output,\n };\n } else if (mergeResult === 'FAILURE') {\n return {\n success: false,\n failedFiles,\n validationStatus: validationStatus || 'NOT_RUN',\n reason,\n notes,\n output,\n };\n } else {\n // No structured result markers found - try to detect human-readable format\n // Agents sometimes output \"MERGE TASK COMPLETE\" instead of \"MERGE_RESULT: SUCCESS\"\n const lowerOutput = output.toLowerCase();\n\n // Check for success indicators\n const successIndicators = [\n 'merge task complete',\n 'successfully merged',\n 'merge complete',\n 'pushed merge commit',\n 'successfully merged and pushed',\n ];\n\n const failureIndicators = [\n 'merge failed',\n 'merge task failed',\n 'could not merge',\n 'conflict not resolved',\n ];\n\n const hasSuccessIndicator = successIndicators.some(i => lowerOutput.includes(i));\n const hasFailureIndicator = failureIndicators.some(i => lowerOutput.includes(i));\n\n if (hasSuccessIndicator && !hasFailureIndicator) {\n // Extract test status from output if mentioned\n let detectedTestStatus: 'PASS' | 'FAIL' | 'SKIP' = 'SKIP';\n if (lowerOutput.includes('tests: pass') || lowerOutput.includes('tests passed') ||\n output.match(/\\d+ passed/)) {\n detectedTestStatus = 'PASS';\n } else if (lowerOutput.includes('tests: fail') || lowerOutput.includes('tests failed')) {\n detectedTestStatus = 'FAIL';\n }\n\n console.log('[merge-agent] Detected success from human-readable output');\n return {\n success: true,\n testsStatus: detectedTestStatus,\n validationStatus: 'PASS',\n notes: 'Detected from human-readable output (agent did not use structured format)',\n output,\n };\n }\n\n if (hasFailureIndicator) {\n console.log('[merge-agent] Detected failure from human-readable output');\n return {\n success: false,\n validationStatus: 'NOT_RUN',\n reason: 'Detected merge failure from agent output',\n output,\n };\n }\n\n // Truly unrecognized output\n return {\n success: false,\n validationStatus: 'NOT_RUN',\n reason: 'Agent did not report result in expected format',\n output,\n };\n }\n}\n\n/**\n * Get conflict files from git status (async)\n */\nasync function getConflictFiles(projectPath: string): Promise<string[]> {\n try {\n const { stdout: status } = await execAsync('git diff --name-only --diff-filter=U', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n\n return status\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n } catch (error) {\n console.error('Failed to get conflict files:', error);\n return [];\n }\n}\n\nasync function resolveMainPreferredSyncConflicts(\n projectPath: string,\n conflictFiles: string[],\n): Promise<{ success: boolean; reason?: string }> {\n if (conflictFiles.length === 0 || !conflictFiles.every(isSyncMainMainPreferredPath)) {\n return { success: false, reason: 'conflicts include non-pipeline-owned files' };\n }\n\n try {\n for (const path of SYNC_MAIN_MAIN_PREFERRED_PATHS) {\n await execAsync(`git rm -r --quiet --ignore-unmatch -- ${path}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n await execAsync(`git checkout origin/main -- ${path}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n }).catch(() => {\n // The path may not exist on origin/main. In that case, the preceding\n // git rm records main's deletion for this pipeline-owned path.\n });\n await execAsync(`git add -A -- ${path}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n }\n\n const remainingConflicts = await getConflictFiles(projectPath);\n if (remainingConflicts.length > 0) {\n return {\n success: false,\n reason: `Unresolved conflicts remain: ${remainingConflicts.join(', ')}`,\n };\n }\n\n await execAsync('git commit --no-edit', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n return { success: true };\n } catch (error: any) {\n return {\n success: false,\n reason: `Failed to auto-resolve pipeline-owned conflicts: ${error.message}`,\n };\n }\n}\n\n/**\n * Log merge to history\n */\nexport function logMergeHistory(context: MergeConflictContext, result: MergeResult, sessionId?: string): void {\n // Ensure history directory exists\n if (!existsSync(MERGE_HISTORY_DIR)) {\n mkdirSync(MERGE_HISTORY_DIR, { recursive: true });\n }\n\n const entry: MergeHistoryEntry = {\n timestamp: new Date().toISOString(),\n issueId: context.issueId,\n sourceBranch: context.sourceBranch,\n targetBranch: context.targetBranch,\n conflictFiles: context.conflictFiles,\n result: {\n ...result,\n output: undefined, // Don't store full output in history\n },\n sessionId,\n };\n\n appendFileSync(MERGE_HISTORY_FILE, JSON.stringify(entry) + '\\n', 'utf-8');\n}\n\n/**\n * Log activity to the dashboard activity log (event-sourced via emitActivityEntry)\n */\nfunction logActivity(action: string, details: string, issueId?: string): void {\n emitActivityEntrySync({\n source: 'ship',\n level: action.includes('fail') || action.includes('error') ? 'error' : action.includes('warn') ? 'warn' : 'success',\n message: details,\n issueId,\n });\n}\n\n/**\n * Voice-worthy merge milestone announcement. Messages start with one of three\n * distinctive prefixes (\"Merge started\", \"Merge completed\", \"Merge failed\") so\n * pan-tts can filter merge-agent chatter and speak only these three events.\n *\n * Do not change these prefixes without updating the pan-tts filter\n * (~/Projects/pan-tts/src/pan_tts/__main__.py — ALLOWED_MERGE_PREFIXES).\n */\nfunction announceMerge(\n status: 'started' | 'completed' | 'failed',\n issueId: string,\n extra?: string,\n): void {\n const prefix = status === 'started'\n ? 'Merge started'\n : status === 'completed'\n ? 'Merge completed'\n : 'Merge failed';\n const tail = extra ? `. ${extra}` : '';\n emitActivityEntrySync({\n source: 'ship',\n level: status === 'failed' ? 'error' : 'success',\n message: `${prefix} for ${issueId}${tail}`,\n issueId,\n });\n // Upleveled TTS utterance — short, speakable, no issue prefix noise\n const ttsUtterance = status === 'started'\n ? `Starting merge for ${issueId}`\n : status === 'completed'\n ? `${issueId} merged to main`\n : `Merge failed for ${issueId}`;\n emitActivityTtsSync({\n utterance: ttsUtterance,\n priority: status === 'failed' ? 0 : 1,\n issueId,\n source: 'merge-agent',\n eventType: `mergeStatus.${status === 'completed' ? 'merged' : status === 'started' ? 'merging' : 'failed'}`,\n });\n}\n\n/**\n * Capture tmux output and look for result markers (async)\n */\nexport async function captureTmuxOutput(sessionName: string): Promise<string> {\n try {\n return await Effect.runPromise(capturePane(sessionName));\n } catch {\n return '';\n }\n}\n\n/** Patterns to match in tmux capture-pane output (git push/fetch lines) */\nexport const GIT_PATTERNS: Array<{ re: RegExp; operation: GitOperationType; level: 'info' | 'warn' | 'error' }> = [\n { re: /force-with-lease/i, operation: 'force_push_cmd', level: 'warn' },\n { re: /git push/i, operation: 'push_attempt', level: 'info' },\n { re: /git fetch/i, operation: 'fetch_attempt', level: 'info' },\n { re: /\\[rejected\\]/i, operation: 'push_rejected', level: 'error' },\n { re: /non-fast-forward/i, operation: 'non_ff', level: 'error' },\n { re: /retrying/i, operation: 'retry', level: 'warn' },\n { re: /\\[remote rejected\\]/i, operation: 'remote_rejected', level: 'error' },\n { re: /Everything up-to-date/i, operation: 'push_noop', level: 'info' },\n];\n\n/**\n * Scan tmux capture-pane output for git push/fetch patterns and emit each\n * as a git_operations row. Uses seenLineHashes to dedupe within a session.\n */\nexport function scanGitPatterns(\n output: string,\n seenLineHashes: Set<string>,\n issueId: string,\n branch?: string,\n): void {\n const lines = output.split('\\n');\n const ts = new Date().toISOString();\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n // Simple hash: first 120 chars (avoids hashing megabytes)\n const hash = trimmed.slice(0, 120);\n if (seenLineHashes.has(hash)) continue;\n\n for (const { re, operation, level } of GIT_PATTERNS) {\n if (re.test(trimmed)) {\n seenLineHashes.add(hash);\n appendGitOperationSync({\n operation,\n branch,\n issueId,\n status: level === 'error' ? 'failure' : 'success',\n error: level !== 'info' ? trimmed.slice(0, 200) : undefined,\n ts,\n });\n emitActivityEntrySync({\n source: 'ship',\n level,\n message: `[git] ${trimmed.slice(0, 100)}`,\n issueId,\n });\n break; // only match one pattern per line\n }\n }\n }\n}\n\n/**\n * Check if specialist-merge-agent tmux session is running (async)\n */\nexport async function isMergeAgentRunning(): Promise<boolean> {\n return Effect.runPromise(sessionExists('specialist-merge-agent'));\n}\n\n/**\n * Send a message to an agent's tmux session (async)\n */\nexport async function sendMessageToAgent(issueId: string, message: string): Promise<boolean> {\n // Agent sessions are typically named agent-{issueId} (lowercase)\n const sessionName = `agent-${issueId.toLowerCase()}`;\n\n try {\n // Check if session exists\n if (!await Effect.runPromise(sessionExists(sessionName))) {\n console.log(`[merge-agent] Could not send message to ${sessionName} (session does not exist)`);\n return false;\n }\n\n // Send the message using centralized sendKeys\n await Effect.runPromise(sendKeys(sessionName, message));\n\n console.log(`[merge-agent] Sent message to ${sessionName}`);\n logActivity('agent_message', `Sent to ${sessionName}: ${message.slice(0, 100)}...`);\n return true;\n } catch {\n console.log(`[merge-agent] Could not send message to ${sessionName} (session may not exist)`);\n return false;\n }\n}\n\n// PAN-1531: ship-role machinery (buildShipPreparationPrompt, buildShipSyncMainPrompt,\n// spawnShipRoleForTask, spawnMergeAgentForBranches, spawnRebaseAgentForBranch,\n// defaultWorkspaceForIssue) removed. Rebase is now performed in-process via\n// rebaseFeatureBranch() in src/lib/cloister/merge-rebase.ts. See docs/MERGE-WORKFLOW.md.\n\nexport async function salvageStrandedMerge(\n projectPath: string,\n targetBranch: string,\n headBefore: string,\n issueId: string,\n logActivity: (action: string, detail: string) => void,\n): Promise<{ success: boolean; reason?: string } | null> {\n try {\n const { stdout: currentHeadRaw } = await execAsync('git rev-parse HEAD', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n const currentHead = currentHeadRaw.trim();\n\n if (currentHead === headBefore) {\n // No local merge happened — nothing to salvage\n return null;\n }\n\n // Local HEAD changed — check if it's ahead of remote\n await execAsync(`git fetch origin ${targetBranch}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n timeout: 10000,\n }).catch(() => {});\n\n const { stdout: remoteHeadRaw } = await execAsync(`git rev-parse origin/${targetBranch}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n\n if (remoteHeadRaw.trim() === currentHead) {\n // Already pushed (maybe by another process)\n console.log(`[merge-agent] Salvage check: merge already pushed`);\n return { success: true };\n }\n\n // Stranded merge detected — push it (with divergence guard to protect hotfixes)\n console.log(`[merge-agent] SALVAGING stranded merge for ${issueId}: local HEAD ${currentHead.slice(0, 8)} != remote ${remoteHeadRaw.trim().slice(0, 8)}`);\n logActivity('merge_salvage', `Pushing stranded merge commit ${currentHead.slice(0, 8)} for ${issueId}`);\n\n try {\n await Effect.runPromise(gitPush(projectPath, 'origin', targetBranch, { issueId }));\n } catch (pushErr: unknown) {\n if (pushErr instanceof MainDivergedError) {\n // origin has advanced past our local ancestor — a hotfix landed.\n // Mark stuck so Deacon won't re-trigger, then let the caller handle it.\n markWorkspaceStuck(issueId, 'main_diverged', {\n localSha: pushErr.localSha,\n remoteSha: pushErr.remoteSha,\n });\n logActivity('merge_salvage_diverged', `Salvage aborted: origin/${targetBranch} diverged (remote ${pushErr.remoteSha.slice(0, 7)} not ancestor of local ${pushErr.localSha.slice(0, 7)})`);\n return { success: false, reason: pushErr.message };\n }\n throw pushErr;\n }\n\n console.log(`[merge-agent] Salvage push successful for ${issueId}`);\n logActivity('merge_salvage_success', `Stranded merge pushed successfully`);\n return { success: true };\n } catch (error: any) {\n console.error(`[merge-agent] Salvage failed: ${error.message}`);\n logActivity('merge_salvage_failed', `Salvage push failed: ${error.message}`);\n return null;\n }\n}\n\n/**\n * Result of syncing main into a workspace branch\n */\nexport interface SyncMainResult {\n success: boolean;\n alreadyUpToDate?: boolean;\n commitCount?: number;\n changedFiles?: string[];\n conflictFiles?: string[];\n reason?: string;\n}\n\n/**\n * Scan workspace for leftover git conflict markers (async)\n */\nexport async function scanForConflictMarkers(projectPath: string): Promise<string[]> {\n try {\n // git diff --check exits non-zero and prints filenames when conflict markers exist\n const { stdout } = await execAsync('git diff --check 2>&1 || true', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n const files = stdout\n .split('\\n')\n .filter(line => line.includes('leftover conflict marker'))\n .map(line => line.split(':')[0].trim())\n .filter(f => f.length > 0);\n return [...new Set(files)];\n } catch {\n return [];\n }\n}\n\n/**\n * Sync the latest main branch into a workspace's feature branch.\n *\n * This performs a `git merge origin/main` in the workspace. If the merge is clean\n * it returns immediately. If conflicts arise, the conflict details are surfaced\n * for manual workspace resolution. The merge is never pushed — this is a local\n * workspace operation.\n *\n * Auto-commits any uncommitted changes before merging (with safety verification).\n */\nexport async function syncMainIntoWorkspace(\n projectPath: string,\n issueId: string,\n): Promise<SyncMainResult> {\n console.log(`[sync-main] Starting sync of main into workspace for ${issueId}`);\n logActivity('sync_main_start', `Starting sync for ${issueId}`);\n\n // PAN-1158 safety net: a workspace bd dolt DB that briefly went empty can\n // leave `.beads/issues.jsonl` reported as deleted by `git status`. The\n // auto-commit below would then propagate that deletion onto the feature\n // branch. Restore the tracked export first so the auto-commit only sees\n // intentional changes.\n await Effect.runPromise(restoreTrackedBeadsExport(projectPath));\n\n // Pre-flight: auto-commit uncommitted changes before merge\n console.log(`[sync-main] Checking for uncommitted changes...`);\n logActivity('sync_main_auto_commit', `Auto-committing uncommitted changes before sync`);\n const autoCommit = await autoCommitWorkspaceChangesBeforeSync(projectPath);\n if (!autoCommit.success) {\n const message = autoCommit.reason || 'Failed to auto-commit uncommitted changes';\n console.error(`[sync-main] ${message}`);\n logActivity('sync_main_blocked', message);\n return { success: false, reason: message };\n }\n if (autoCommit.committed) {\n console.log(`[sync-main] Auto-commit successful`);\n }\n\n // Verify no non-excluded uncommitted changes remain.\n try {\n const { stdout: postCommitStatus } = await execAsync('git status --porcelain', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n if (postCommitStatus.trim()) {\n const remainingNonExcluded = postCommitStatus\n .trim()\n .split('\\n')\n .filter((line) => !isAutoCommitExcludedPath(parseStatusPath(line)));\n if (remainingNonExcluded.length > 0) {\n const message = 'Uncommitted changes remain after auto-commit — aborting sync';\n console.error(`[sync-main] ${message}`);\n logActivity('sync_main_blocked', message);\n return { success: false, reason: message };\n }\n }\n } catch (error: any) {\n return { success: false, reason: `Failed to check git status: ${error.message}` };\n }\n\n // Clean up stale git lock files\n try {\n const lockCleanup = await Effect.runPromise(cleanupStaleLocks(projectPath));\n if (lockCleanup.found.length > 0) {\n console.log(`[sync-main] Found ${lockCleanup.found.length} lock file(s)`);\n if (lockCleanup.removed.length > 0) {\n console.log(`[sync-main] Cleaned up ${lockCleanup.removed.length} stale lock file(s)`);\n logActivity('git_lock_cleanup', `Removed ${lockCleanup.removed.length} stale lock file(s)`);\n }\n if (lockCleanup.errors.some((e: { file: string; error: string }) => e.error.includes('Git processes are running'))) {\n const message = 'Git processes are still running — cannot safely start sync';\n console.error(`[sync-main] ${message}`);\n logActivity('sync_main_blocked', message);\n return { success: false, reason: message };\n }\n }\n } catch (lockErr: any) {\n console.warn(`[sync-main] Lock cleanup warning: ${lockErr.message} (continuing)`);\n }\n\n // Fetch latest main\n try {\n console.log(`[sync-main] Fetching origin/main...`);\n await execAsync('git fetch origin main', { cwd: projectPath, encoding: 'utf-8' });\n } catch (error: any) {\n return { success: false, reason: `Failed to fetch origin/main: ${error.message}` };\n }\n\n // Attempt the merge\n let mergeOutput = '';\n let hasConflicts = false;\n try {\n const result = await execAsync('git merge origin/main', { cwd: projectPath, encoding: 'utf-8' });\n mergeOutput = (result.stdout || '') + (result.stderr || '');\n } catch (error: any) {\n mergeOutput = (error.stdout || '') + (error.stderr || '');\n hasConflicts = true;\n }\n\n // Already up to date?\n if (mergeOutput.includes('Already up to date') || mergeOutput.includes('Already up-to-date')) {\n console.log(`[sync-main] Already up to date`);\n logActivity('sync_main_noop', `${issueId} already up to date with main`);\n return { success: true, alreadyUpToDate: true };\n }\n\n if (!hasConflicts) {\n // Clean merge — collect stats\n console.log(`[sync-main] Clean merge completed`);\n logActivity('sync_main_success', `Clean merge of main into ${issueId}`);\n\n let changedFiles: string[] = [];\n let commitCount = 0;\n try {\n const { stdout: diffFiles } = await execAsync(\n 'git diff --name-only ORIG_HEAD HEAD 2>/dev/null || git diff --name-only HEAD~1 HEAD',\n { cwd: projectPath, encoding: 'utf-8' },\n );\n changedFiles = diffFiles.trim().split('\\n').filter(f => f.length > 0);\n } catch { /* non-fatal */ }\n try {\n const { stdout: logOut } = await execAsync(\n 'git log ORIG_HEAD..HEAD --oneline 2>/dev/null || echo \"\"',\n { cwd: projectPath, encoding: 'utf-8' },\n );\n commitCount = logOut.trim().split('\\n').filter(l => l.length > 0).length;\n } catch { /* non-fatal */ }\n\n return { success: true, commitCount, changedFiles };\n }\n\n // PAN-1531: sync-main conflict case — surface to operator instead of\n // delegating to an LLM ship role. Abort the merge so the working tree is\n // clean, then return the conflict files for the caller to display.\n const conflictFiles = await getConflictFiles(projectPath);\n const mainPreferredResolution = await resolveMainPreferredSyncConflicts(projectPath, conflictFiles);\n if (mainPreferredResolution.success) {\n console.log(`[sync-main] Auto-resolved ${conflictFiles.length} pipeline-owned conflict(s) with origin/main`);\n logActivity('sync_main_auto_resolved_conflicts', `Auto-resolved ${conflictFiles.length} pipeline-owned conflict(s) in ${issueId} with origin/main`);\n\n let changedFiles: string[] = [];\n let commitCount = 0;\n try {\n const { stdout: diffFiles } = await execAsync(\n 'git diff --name-only ORIG_HEAD HEAD 2>/dev/null || git diff --name-only HEAD~1 HEAD',\n { cwd: projectPath, encoding: 'utf-8' },\n );\n changedFiles = diffFiles.trim().split('\\n').filter(f => f.length > 0);\n } catch { /* non-fatal */ }\n try {\n const { stdout: logOut } = await execAsync(\n 'git log ORIG_HEAD..HEAD --oneline 2>/dev/null || echo \"\"',\n { cwd: projectPath, encoding: 'utf-8' },\n );\n commitCount = logOut.trim().split('\\n').filter(l => l.length > 0).length;\n } catch { /* non-fatal */ }\n\n return { success: true, commitCount, changedFiles };\n }\n\n console.log(`[sync-main] ${conflictFiles.length} conflict(s); aborting merge for manual resolution`);\n logActivity('sync_main_conflicts', `${conflictFiles.length} conflict(s) in ${issueId}: ${conflictFiles.join(', ')}`);\n\n try { await execAsync('git merge --abort', { cwd: projectPath, encoding: 'utf-8' }); } catch { /* non-fatal */ }\n\n return {\n success: false,\n conflictFiles,\n reason: `Sync-main produced ${conflictFiles.length} conflict(s) in ${issueId}: ${conflictFiles.join(', ')}. Resolve manually in the workspace, then re-run sync-main.`,\n };\n\n}\n\n/**\n * Look up and run quality gates for the project at projectPath.\n * Returns empty array if no quality gates are configured.\n *\n * In polyrepo mode (projectPath is a sub-repo of project.path), only gates\n * whose `path` field matches the relative sub-repo path are run. Gates with\n * no `path` field are skipped in polyrepo context.\n */\nexport async function runProjectQualityGates(\n projectPath: string,\n phase: 'pre_push' | 'post_push'\n): Promise<import('./validation.js').QualityGateResult[]> {\n try {\n const config = loadProjectsConfigSync();\n // Find the project whose path matches\n const project = Object.values(config.projects).find(p => projectPath.startsWith(p.path));\n if (!project?.quality_gates || Object.keys(project.quality_gates).length === 0) {\n console.log(`[merge-agent] No quality gates configured for ${projectPath}`);\n return [];\n }\n\n // Detect polyrepo context: if projectPath is a subdirectory of project.path,\n // repoRelPath is non-empty (e.g., 'frontend' or 'backend').\n const repoRelPath = relative(project.path, projectPath);\n const matchedRepo = project.workspace?.repos?.find(repo => (\n repoRelPath === repo.path ||\n projectPath === join(project.path, repo.path)\n ));\n const repoIdentifiers = matchedRepo\n ? new Set([matchedRepo.path, matchedRepo.name])\n : new Set(repoRelPath && !repoRelPath.startsWith('..') ? [repoRelPath] : []);\n\n let gatesToRun = project.quality_gates;\n if (repoRelPath && !repoRelPath.startsWith('..')) {\n // Polyrepo: gates can target either the repo path (\"frontend\") or the\n // configured repo key/alias (\"fe\"). Both map to the same sub-repo.\n const filtered = Object.entries(project.quality_gates).filter(\n ([, gate]) => gate.path && repoIdentifiers.has(gate.path)\n );\n if (filtered.length === 0) {\n console.log(`[merge-agent] No quality gates configured for repo path \"${repoRelPath}\"`);\n return [];\n }\n gatesToRun = Object.fromEntries(filtered);\n console.log(\n `[merge-agent] Polyrepo: running ${Object.keys(gatesToRun).length} gate(s) for path \"${repoRelPath}\" (${[...repoIdentifiers].join(', ')})`\n );\n }\n\n console.log(`[merge-agent] Running ${phase} quality gates for project \"${project.name}\"`);\n return await Effect.runPromise(runQualityGates(gatesToRun, projectPath, phase));\n } catch (error: any) {\n console.error(`[merge-agent] Failed to load quality gates: ${error.message}`);\n return [];\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"merge-agent-B9hIZ6Z0.js","names":["execAsync"],"sources":["../src/lib/cloister/validation.ts","../src/lib/overdeck/git-activity.ts","../src/lib/git-activity.ts","../src/lib/git/operations.ts","../src/lib/cloister/merge-agent.ts"],"sourcesContent":["/**\n * Merge Validation - Validation utilities for merge completeness\n *\n * Validates that merged code:\n * - Has no conflict markers\n * - Builds successfully\n * - Passes all tests\n */\n\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { join } from 'path';\nimport { existsSync } from 'fs';\nimport { Effect } from 'effect';\nimport type { QualityGateConfig, TemplatePlaceholders } from '../workspace-config.js';\nimport { replacePlaceholdersSync } from '../workspace-config.js';\nimport { loadConfigSync } from '../config.js';\nimport { GitError } from '../errors.js';\n\nconst execAsync = promisify(exec);\nconst DASHBOARD_RUNTIME_ENV_KEYS = ['API_PORT', 'PORT', 'DASHBOARD_URL'] as const;\n\nfunction buildQualityGateEnv(gateEnv: Record<string, string> | undefined): NodeJS.ProcessEnv {\n const env = { ...process.env };\n for (const key of DASHBOARD_RUNTIME_ENV_KEYS) {\n if (!Object.prototype.hasOwnProperty.call(gateEnv ?? {}, key)) {\n delete env[key];\n }\n }\n // PAN-1678: verification and ship builds run `npm run build` as a quality\n // gate, which by default includes the heavy ~10-core build:docs-index job\n // (a docs *search* index, irrelevant to whether the issue's code\n // compiles/tests). Run concurrently across agents it stampedes the host to\n // OOM. Default it off for every gate command so gate builds stay cheap; a\n // gate may still opt back in via gate.env. The skip mechanism lives in\n // scripts/build-post-cli.mjs (PAN-1659). Release/publish builds the index\n // through a separate path (build-for-publish.mjs) that never sets this.\n return { SKIP_DOCS_INDEX: '1', ...env, ...gateEnv };\n}\n\n/**\n * Context for validation execution\n */\nexport interface ValidationContext {\n /** Project root path */\n projectPath: string;\n /** Issue ID for logging */\n issueId?: string;\n /** Custom validation script path (defaults to scripts/validate-merge.sh) */\n validationScript?: string;\n /** Baseline test failure count for comparison mode (pre-existing failures) */\n baselineTestFailures?: number;\n}\n\n/**\n * Detailed validation failure information\n */\nexport interface ValidationFailure {\n /** Type of failure: conflict, build, or test */\n type: 'conflict' | 'build' | 'test';\n /** Files affected (for conflicts) */\n files?: string[];\n /** Error message or output */\n message: string;\n}\n\n/**\n * Result of validation execution\n */\nexport interface ValidationResult {\n /** Overall validation success */\n success: boolean;\n /** Validation passed (or skipped — check `skipped` to distinguish) */\n valid: boolean;\n /** Validation was skipped (no validation script found) */\n skipped?: boolean;\n /** Conflict markers detected */\n conflictMarkersFound: boolean;\n /** Build result */\n buildPassed: boolean | null; // null if not run\n /** Test result */\n testsPassed: boolean | null; // null if not run\n /** List of failures */\n failures: ValidationFailure[];\n /** Raw validation output */\n output: string;\n /** Error message if validation script itself failed */\n error?: string;\n}\n\n/**\n * Parse validation script output to extract structured results\n */\nfunction parseValidationOutput(output: string, exitCode: number): ValidationResult {\n const lines = output.split('\\n');\n\n const failures: ValidationFailure[] = [];\n let conflictMarkersFound = false;\n let buildPassed: boolean | null = null;\n let testsPassed: boolean | null = null;\n\n // Track what stage we're in\n let inConflictCheck = false;\n let inBuildCheck = false;\n let inTestCheck = false;\n\n const conflictFiles: string[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Detect stages\n if (trimmed.startsWith('Checking for conflict markers')) {\n inConflictCheck = true;\n inBuildCheck = false;\n inTestCheck = false;\n } else if (trimmed.startsWith('Running build')) {\n inConflictCheck = false;\n inBuildCheck = true;\n inTestCheck = false;\n } else if (trimmed.startsWith('Running tests')) {\n inConflictCheck = false;\n inBuildCheck = false;\n inTestCheck = true;\n }\n\n // Parse conflict markers\n if (inConflictCheck) {\n if (trimmed.startsWith('ERROR: Conflict')) {\n conflictMarkersFound = true;\n } else if (trimmed.includes('/') && !trimmed.startsWith('ERROR')) {\n // File path listed\n conflictFiles.push(trimmed);\n } else if (trimmed.startsWith('✓ No conflict markers found')) {\n conflictMarkersFound = false;\n }\n }\n\n // Parse build result\n if (inBuildCheck) {\n if (trimmed.startsWith('✓ Build passed')) {\n buildPassed = true;\n } else if (trimmed.startsWith('ERROR: Build failed') ||\n trimmed.includes('VALIDATION FAILED: Build errors detected')) {\n buildPassed = false;\n } else if (trimmed.includes('skipping build check')) {\n buildPassed = null; // Not applicable\n }\n }\n\n // Parse test result\n if (inTestCheck) {\n if (trimmed.startsWith('✓ Tests passed')) {\n testsPassed = true;\n } else if (trimmed.startsWith('ERROR: Tests failed') ||\n trimmed.includes('VALIDATION FAILED: Test failures detected')) {\n testsPassed = false;\n } else if (trimmed.includes('skipping test check')) {\n testsPassed = null; // Not applicable\n }\n }\n }\n\n // Build failures list\n if (conflictMarkersFound) {\n failures.push({\n type: 'conflict',\n files: conflictFiles.length > 0 ? conflictFiles : undefined,\n message: 'Conflict markers detected in merged code',\n });\n }\n\n if (buildPassed === false) {\n failures.push({\n type: 'build',\n message: 'Build failed after merge',\n });\n }\n\n if (testsPassed === false) {\n failures.push({\n type: 'test',\n message: 'Tests failed after merge',\n });\n }\n\n // Determine overall validity\n const valid = exitCode === 0 &&\n !conflictMarkersFound &&\n (buildPassed === null || buildPassed === true) &&\n (testsPassed === null || testsPassed === true);\n\n return {\n success: true, // Script ran successfully\n valid,\n conflictMarkersFound,\n buildPassed,\n testsPassed,\n failures,\n output,\n };\n}async function runMergeValidationPromise(\n context: ValidationContext\n): Promise<ValidationResult> {\n const { projectPath, validationScript } = context;\n\n // Determine validation script path\n const scriptPath = validationScript || join(projectPath, 'scripts', 'validate-merge.sh');\n\n // No validation script = skip validation (specialist already ran build + tests)\n if (!existsSync(scriptPath)) {\n console.log(`[validation] No validation script at ${scriptPath}, skipping (specialist already validated)`);\n return {\n success: true,\n valid: true,\n skipped: true,\n conflictMarkersFound: false,\n buildPassed: null,\n testsPassed: null,\n failures: [],\n output: '',\n };\n }\n\n console.log(`[validation] Running validation script: ${scriptPath}`);\n console.log(`[validation] Project path: ${projectPath}`);\n\n try {\n // Run validation script\n // Pass baseline failures as env var for baseline comparison mode\n const env = { ...process.env };\n if (context.baselineTestFailures !== undefined) {\n env.BASELINE_FAILURES = String(context.baselineTestFailures);\n console.log(`[validation] Baseline comparison mode: ${context.baselineTestFailures} pre-existing failures`);\n }\n\n const { stdout, stderr } = await execAsync(\n `bash \"${scriptPath}\" \"${projectPath}\"`,\n {\n cwd: projectPath,\n env,\n maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs\n timeout: 10 * 60 * 1000, // 10 minute timeout\n }\n );\n\n const output = stdout + stderr;\n\n console.log(`[validation] ✓ Validation passed`);\n\n return parseValidationOutput(output, 0);\n } catch (error: any) {\n // Validation script exited with non-zero code (validation failed)\n const exitCode = error.code || 1;\n const output = (error.stdout || '') + (error.stderr || '');\n\n console.log(`[validation] ✗ Validation failed (exit code ${exitCode})`);\n\n // Parse the output to understand what failed\n const result = parseValidationOutput(output, exitCode);\n\n return result;\n }\n}async function autoRevertMergePromise(projectPath: string): Promise<boolean> {\n console.log(`[validation] Auto-reverting merge in ${projectPath}`);\n\n try {\n // Get current commit before revert (for logging)\n const { stdout: beforeCommit } = await execAsync('git rev-parse HEAD', {\n cwd: projectPath,\n });\n\n // Use ORIG_HEAD — git sets this to pre-merge HEAD at merge time.\n // Handles fast-forwards, multi-commit merges, and any commits\n // added to main between task start and merge execution.\n await execAsync('git reset --hard ORIG_HEAD', {\n cwd: projectPath,\n });\n\n // Get new HEAD after revert (for logging)\n const { stdout: afterCommit } = await execAsync('git rev-parse HEAD', {\n cwd: projectPath,\n });\n\n console.log(\n `[validation] ✓ Auto-revert successful: ${beforeCommit.trim()} -> ${afterCommit.trim()} (via ORIG_HEAD)`\n );\n\n return true;\n } catch (error: any) {\n console.error(`[validation] ✗ Auto-revert failed:`, error.message);\n return false;\n }\n}\n\n/**\n * Result of a single quality gate execution\n */\nexport interface QualityGateResult {\n /** Gate name from projects.yaml */\n name: string;\n /** Whether the gate passed */\n passed: boolean;\n /** Whether the gate was required */\n required: boolean;\n /** Gate output (stdout + stderr) */\n output: string;\n /** Duration in milliseconds */\n durationMs: number;\n /** Error message if gate failed */\n error?: string;\n /** PAN-2461: the gate could not run at all (e.g. its container is not running).\n * Callers must treat this as retryable infrastructure failure — never as a\n * code failure, and never count it against verification attempt budgets. */\n infraUnavailable?: boolean;\n}\n\n/**\n * Options for running quality gates\n */\nexport interface QualityGateRunOptions {\n /** Whether the workspace is remote (SSH) */\n isRemote?: boolean;\n /** VM name for SSH connections (required when isRemote is true) */\n vmName?: string;\n /** Template placeholders for resolving container names (e.g., {{FEATURE_FOLDER}}) */\n placeholders?: TemplatePlaceholders;\n /** PAN-2487: mirror of the human-readable gate progress lines (start/pass/fail\n * per gate), e.g. into the per-issue ship log so the UI can show live progress. */\n onLog?: (line: string) => void;\n}\n\n/**\n * Default quality gates used when no quality_gates config exists in projects.yaml.\n * Runs typecheck → lint → changed-file tests sequentially (bail on first failure).\n * Heavy e2e/browser suites belong in project-specific CI-only or @slow tiers.\n */\nexport const DEFAULT_GATES: Record<string, QualityGateConfig> = {\n typecheck: { command: 'npm run typecheck 2>&1' },\n lint: { command: 'npm run lint 2>&1' },\n test: {\n command: 'OVERDECK_VERIFICATION=1 npx vitest run --changed {{CHANGED_BASE}} && npm --prefix ./src/dashboard/frontend run test -- src/lib/__tests__/issueActions.test.ts src/lib/__tests__/issueActions.no-actions-lost.test.ts src/lib/__tests__/issueActions.parity.test.tsx',\n },\n};async function runQualityGatesPromise(\n gates: Record<string, QualityGateConfig>,\n projectPath: string,\n phase: 'pre_push' | 'post_push' = 'pre_push',\n opts: QualityGateRunOptions = {}\n): Promise<QualityGateResult[]> {\n if (opts.isRemote && !opts.vmName) {\n throw new Error('Remote workspace requires vmName');\n }\n if (opts.isRemote && opts.vmName) {\n // Validate vmName and projectPath to prevent shell injection.\n // Both are controlled by Overdeck config, but explicit validation\n // catches any accidental or malicious values before they reach the shell.\n if (!/^[a-z0-9][a-z0-9-]*$/.test(opts.vmName)) {\n throw new Error(`Invalid vmName for SSH: ${opts.vmName}`);\n }\n if (!/^[a-zA-Z0-9/_\\-.]+$/.test(projectPath)) {\n throw new Error(`Workspace path contains unsafe characters: ${projectPath}`);\n }\n }\n const results: QualityGateResult[] = [];\n\n for (const [name, gate] of Object.entries(gates)) {\n const gatePhase = gate.phase || 'pre_push';\n if (gatePhase !== phase) continue;\n\n const required = gate.required !== false; // default true\n const cwd = gate.path ? join(projectPath, gate.path) : projectPath;\n\n console.log(`[quality-gate] Running \"${name}\" (${required ? 'required' : 'optional'}) in ${cwd}`);\n opts.onLog?.(`Running gate \"${name}\"${required ? '' : ' (optional)'}…`);\n const startTime = Date.now();\n\n if (gate.type === 'http_health') {\n // HTTP health check gate\n const result = await runHttpHealthGate(name, gate, required);\n results.push(result);\n if (!result.passed && required) {\n console.log(`[quality-gate] ✗ Required gate \"${name}\" failed — stopping`);\n break;\n }\n continue;\n }\n\n // Command gate (default)\n\n // PAN-666: substitute {{...}} placeholders into the gate command before\n // resolving it for any execution path. This is what lets a test gate opt into\n // changed-file scoping via `vitest run --changed {{CHANGED_BASE}}` so it runs\n // only the tests affected by the PR and skips pre-existing failures in files\n // the change never touched.\n const command = opts.placeholders\n ? replacePlaceholdersSync(gate.command, opts.placeholders)\n : gate.command;\n\n // For remote workspaces, build and validate the SSH command BEFORE entering\n // the try/catch so validation errors propagate as real errors (not gate failures).\n const isRemote = opts.isRemote && opts.vmName;\n let resolvedCommand: string;\n if (isRemote) {\n // Validate cwd (which may include gate.path) — not just the base projectPath.\n // A gate.path like \"frontend;rm -rf /\" would produce an unsafe cwd after join.\n if (!/^[a-zA-Z0-9/_\\-.]+$/.test(cwd)) {\n throw new Error(`Gate \"${name}\" path resolves to unsafe characters for SSH: ${cwd}`);\n }\n // Validate the command doesn't contain double quotes — a \" in the command would\n // end the SSH double-quoted string and allow local command injection:\n // ssh host \"cd /path && legit; injected\" ← breaks when command contains \"\n if (command.includes('\"')) {\n throw new Error(`Gate \"${name}\" command contains double quotes which are unsafe in SSH context`);\n }\n const flyAppName = loadConfigSync().remote?.fly?.app ?? 'pan-workspaces';\n resolvedCommand = `fly ssh console -a ${flyAppName} -C \"cd ${cwd} && ${command}\"`;\n } else if (gate.container && gate.container_name) {\n // Run inside Docker container — resolve container name from placeholders\n let containerName = gate.container_name;\n if (opts.placeholders) {\n containerName = replacePlaceholdersSync(containerName, opts.placeholders);\n }\n // PAN-2461: a missing container previously burned verification attempts as a\n // fake check failure (\"frontend-lint failed\" in 33ms because docker exec had\n // no target). Pre-check and report infra-unavailable instead.\n try {\n const { stdout: liveCheck } = await execAsync(\n `docker ps --filter \"name=^${containerName}$\" --format \"{{.Names}}\"`,\n { timeout: 10_000 },\n );\n if (!liveCheck.split('\\n').map(s => s.trim()).includes(containerName)) {\n console.log(`[quality-gate] ⚠ \"${name}\" skipped: container ${containerName} is not running (infra-unavailable, does not count as a check failure)`);\n results.push({\n name, passed: false, required,\n output: `container ${containerName} is not running`,\n durationMs: Date.now() - startTime,\n error: `container ${containerName} is not running`,\n infraUnavailable: true,\n });\n if (required) break;\n continue;\n }\n } catch {\n // docker itself unreachable — same treatment.\n results.push({\n name, passed: false, required,\n output: 'docker daemon unreachable',\n durationMs: Date.now() - startTime,\n error: 'docker daemon unreachable',\n infraUnavailable: true,\n });\n if (required) break;\n continue;\n }\n // Use -w to set working directory inside the container.\n // The container mounts workspace code at /workspaces/feature/<subdir>,\n // so map the gate.path (e.g., 'fe') to the container's working directory.\n const containerWorkdir = gate.path ? `/workspaces/feature/${gate.path}` : '/workspaces/feature';\n // Pass gate.env as -e flags so env vars reach the container process\n const envFlags = gate.env\n ? Object.entries(gate.env).map(([k, v]) => `-e ${k}=\"${v}\"`).join(' ')\n : '';\n resolvedCommand = `docker exec ${envFlags} -w \"${containerWorkdir}\" \"${containerName}\" ${command}`;\n console.log(`[quality-gate] Running in container: ${containerName} (workdir: ${containerWorkdir})`);\n } else {\n // Wrap the local gate in `nice -n 19 sh -c '<cmd>'` so a CPU-bound\n // build (tsdown/vite) can't starve the dashboard process that is\n // its parent — when it does, the event-loop tick check reports\n // 600ms+ \"stalls\" that are really just scheduler delay.\n if (process.platform === 'win32') {\n resolvedCommand = command;\n } else {\n const escaped = command.replace(/'/g, `'\\\\''`);\n resolvedCommand = `nice -n 19 sh -c '${escaped}'`;\n }\n }\n\n // PAN-2461: gates may declare `retry: N` — CI-parity for suites with known\n // order-dependent flakes. A pass on any attempt is a pass.\n const maxAttempts = 1 + Math.max(0, gate.retry ?? 0);\n let lastError: any = null;\n let passedGate = false;\n let passOutput = '';\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n // When running in container, don't set host cwd (irrelevant)\n const useHostCwd = !isRemote && !(gate.container && gate.container_name);\n const env = buildQualityGateEnv(gate.env);\n const { stdout, stderr } = await execAsync(resolvedCommand, {\n cwd: useHostCwd ? cwd : undefined,\n env,\n maxBuffer: 10 * 1024 * 1024, // 10MB\n timeout: 20 * 60 * 1000, // 20 minute timeout per gate (PAN-1989: a near-total rename makes `vitest --changed` select ~the whole suite — 715 root + 166 frontend test files — which exceeds 10 min)\n });\n passedGate = true;\n passOutput = (stdout + stderr).slice(-2000); // keep last 2KB\n break;\n } catch (error: any) {\n lastError = error;\n if (attempt < maxAttempts) {\n console.log(`[quality-gate] ✗ \"${name}\" failed attempt ${attempt}/${maxAttempts} — retrying (gate declares retry: ${gate.retry})`);\n opts.onLog?.(`↻ ${name} failed attempt ${attempt}/${maxAttempts} — retrying`);\n }\n }\n }\n\n if (passedGate) {\n const durationMs = Date.now() - startTime;\n console.log(`[quality-gate] ✓ \"${name}\" passed (${durationMs}ms)`);\n opts.onLog?.(`✓ ${name} passed (${Math.round(durationMs/1000)}s)`);\n results.push({\n name,\n passed: true,\n required,\n output: passOutput,\n durationMs,\n });\n } else {\n const error: any = lastError;\n const durationMs = Date.now() - startTime;\n const output = ((error.stdout || '') + (error.stderr || '')).slice(-2000);\n console.log(`[quality-gate] ✗ \"${name}\" failed (${durationMs}ms): ${error.message?.slice(0, 200)}`);\n opts.onLog?.(`✗ ${name} FAILED (${Math.round(durationMs/1000)}s): ${error.message?.slice(0, 160)}`);\n results.push({\n name,\n passed: false,\n required,\n output,\n durationMs,\n error: error.message?.slice(0, 500),\n });\n\n if (required) {\n console.log(`[quality-gate] ✗ Required gate \"${name}\" failed — stopping`);\n break;\n }\n }\n }\n\n const passed = results.filter(r => r.passed).length;\n const failed = results.filter(r => !r.passed).length;\n console.log(`[quality-gate] Complete: ${passed} passed, ${failed} failed out of ${results.length} gates`);\n\n return results;\n}\n\n/**\n * Run an HTTP health check gate (for post-push deployment verification)\n */\nasync function runHttpHealthGate(\n name: string,\n gate: QualityGateConfig,\n required: boolean\n): Promise<QualityGateResult> {\n const url = gate.url;\n if (!url) {\n return {\n name,\n passed: false,\n required,\n output: '',\n durationMs: 0,\n error: 'http_health gate missing url',\n };\n }\n\n const waitSeconds = gate.wait || 120;\n const expectStatus = gate.expect_status || 200;\n const startTime = Date.now();\n\n console.log(`[quality-gate] Waiting ${waitSeconds}s for deployment, then checking ${url}`);\n\n // Wait for deployment\n await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));\n\n try {\n const { stdout } = await execAsync(\n `curl -sL -o /dev/null -w '%{http_code}' --max-time 30 '${url}'`,\n { timeout: 60 * 1000 }\n );\n\n const statusCode = parseInt(stdout.trim(), 10);\n const passed = statusCode === expectStatus;\n const durationMs = Date.now() - startTime;\n\n console.log(`[quality-gate] Health check ${url}: ${statusCode} (expected ${expectStatus}) — ${passed ? 'PASS' : 'FAIL'}`);\n\n return {\n name,\n passed,\n required,\n output: `HTTP ${statusCode} from ${url}`,\n durationMs,\n error: passed ? undefined : `Expected HTTP ${expectStatus}, got ${statusCode}`,\n };\n } catch (error: any) {\n return {\n name,\n passed: false,\n required,\n output: error.message || '',\n durationMs: Date.now() - startTime,\n error: `Health check failed: ${error.message?.slice(0, 200)}`,\n };\n }\n}\n\n// ─── Effect variants (PAN-1249) ──────────────────────────────────────────────\n\n/**\n * Effect variant of {@link runMergeValidation}. The Promise version swallows\n * its own errors and returns a structured {@link ValidationResult}, so the\n * Effect form simply lifts it via `Effect.promise`.\n */\nexport const runMergeValidation = (\n context: ValidationContext,\n): Effect.Effect<ValidationResult> =>\n Effect.promise(() => runMergeValidationPromise(context));\n\n/**\n * Effect variant of {@link autoRevertMerge}. Surfaces git failure through a\n * typed {@link GitError} channel instead of returning `false` silently.\n */\nexport const autoRevertMerge = (\n projectPath: string,\n): Effect.Effect<void, GitError> =>\n Effect.tryPromise({\n try: async () => {\n const ok = await autoRevertMergePromise(projectPath);\n if (!ok) throw new Error('autoRevertMerge returned false');\n },\n catch: (cause) =>\n new GitError({\n command: ['git', 'reset', '--hard', 'ORIG_HEAD'],\n stderr: cause instanceof Error ? cause.message : String(cause),\n exitCode: -1,\n cause,\n }),\n });\n\n/**\n * Effect variant of {@link runQualityGates}. Wraps the Promise implementation\n * with `Effect.promise` because the existing function already aggregates per-\n * gate failures into the returned array — it does not throw on gate failure.\n */\nexport const runQualityGates = (\n gates: Record<string, QualityGateConfig>,\n projectPath: string,\n phase: 'pre_push' | 'post_push' = 'pre_push',\n opts: QualityGateRunOptions = {},\n): Effect.Effect<QualityGateResult[]> =>\n Effect.promise(() => runQualityGatesPromise(gates, projectPath, phase, opts));\n","/**\n * Overdeck door for git_operations — persistent log of git push/fetch/merge\n * events. Replaces database/git-operations-db.ts.\n *\n * Uses the hand-maintained overdeck migration as the schema source of truth.\n */\n\nimport { getOverdeckDatabaseSync } from './infra.js';\n\n// ─── Schema bootstrap ─────────────────────────────────────────────────────────\n\nfunction overdeckDb() {\n return getOverdeckDatabaseSync();\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type GitOperationType =\n | 'push'\n | 'force_push'\n | 'fetch'\n | 'merge'\n | 'rev_parse'\n | 'main_diverged'\n | 'push_attempt'\n | 'fetch_attempt'\n | 'push_rejected'\n | 'non_ff'\n | 'force_push_cmd'\n | 'retry'\n | 'remote_rejected'\n | 'push_noop';\n\nexport type GitOperationStatus = 'success' | 'failure' | 'aborted';\n\nexport interface GitOperation {\n id?: number;\n operation: GitOperationType;\n branch?: string;\n issueId?: string;\n beforeSha?: string;\n afterSha?: string;\n remoteSha?: string;\n status: GitOperationStatus;\n error?: string;\n ts: string;\n}\n\nexport interface GitOperationFilter {\n issueId?: string;\n operation?: GitOperationType;\n status?: GitOperationStatus;\n since?: string;\n limit?: number;\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\nexport function appendGitOperationSync(op: Omit<GitOperation, 'id'>): number {\n const result = overdeckDb().prepare(`\n INSERT INTO git_operations (\n operation, branch, issue_id,\n before_sha, after_sha, remote_sha,\n status, error, ts\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n `).run(\n op.operation,\n op.branch ?? null,\n op.issueId ?? null,\n op.beforeSha ?? null,\n op.afterSha ?? null,\n op.remoteSha ?? null,\n op.status,\n op.error ?? null,\n new Date(op.ts).getTime(),\n );\n return result.lastInsertRowid as number;\n}\n\nexport function listGitOperationsSync(filter: GitOperationFilter = {}): GitOperation[] {\n const conditions: string[] = [];\n const params: (string | number)[] = [];\n\n if (filter.issueId) { conditions.push('issue_id = ?'); params.push(filter.issueId); }\n if (filter.operation) { conditions.push('operation = ?'); params.push(filter.operation); }\n if (filter.status) { conditions.push('status = ?'); params.push(filter.status); }\n if (filter.since) { conditions.push('ts >= ?'); params.push(new Date(filter.since).getTime()); }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';\n const limit = filter.limit ? `LIMIT ${filter.limit}` : 'LIMIT 500';\n\n const rows = overdeckDb().prepare(`\n SELECT id, operation, branch, issue_id, before_sha, after_sha, remote_sha, status, error, ts\n FROM git_operations ${where} ORDER BY ts DESC ${limit}\n `).all(...params) as Array<{\n id: number;\n operation: string;\n branch: string | null;\n issue_id: string | null;\n before_sha: string | null;\n after_sha: string | null;\n remote_sha: string | null;\n status: string;\n error: string | null;\n ts: number;\n }>;\n\n return rows.map((row) => ({\n id: row.id,\n operation: row.operation as GitOperationType,\n branch: row.branch ?? undefined,\n issueId: row.issue_id ?? undefined,\n beforeSha: row.before_sha ?? undefined,\n afterSha: row.after_sha ?? undefined,\n remoteSha: row.remote_sha ?? undefined,\n status: row.status as GitOperationStatus,\n error: row.error ?? undefined,\n ts: new Date(row.ts).toISOString(),\n }));\n}\n","/**\n * Git Activity Service (PAN-653)\n *\n * Write path and read path for git operation events persisted to the\n * git_operations SQLite table. Survives dashboard restart — unlike the\n * in-memory activityLog array which is capped at 100 and wiped on restart.\n *\n * All functions use the async-safe getDatabase() which runs under Node 22.\n * NEVER use execSync/readFileSync here — this runs in the dashboard server.\n */\n\nimport { Effect, Data } from 'effect';\nimport {\n appendGitOperationSync,\n listGitOperationsSync,\n} from './overdeck/git-activity.js';\nimport type {\n GitOperation,\n GitOperationFilter,\n GitOperationType,\n GitOperationStatus,\n} from './overdeck/git-activity.js';\n\nexport type { GitOperation, GitOperationFilter, GitOperationType, GitOperationStatus };\nexport { appendGitOperationSync, listGitOperationsSync };\n\n/** A database operation against the git_operations table failed. */\nexport class GitActivityDbError extends Data.TaggedError('GitActivityDbError')<{\n readonly operation: string;\n readonly cause: unknown;\n}> {}\n\n// ─── Effect variants (PAN-1249) ───────────────────────────────────────────────\n\n/**\n * Effect-native appendGitOperation — typed-error variant of the SQLite write.\n * Fails with GitActivityDbError if the insert throws (e.g. DB locked).\n */\nexport const appendGitOperation = (\n op: Omit<GitOperation, 'id'>,\n): Effect.Effect<number, GitActivityDbError> =>\n Effect.try({\n try: () => appendGitOperationSync(op),\n catch: (cause) => new GitActivityDbError({ operation: 'appendGitOperation', cause }),\n });\n\n/**\n * Effect-native listGitOperations — typed-error variant of the SQLite read.\n * Fails with GitActivityDbError on query failure.\n */\nexport const listGitOperations = (\n filter: GitOperationFilter = {},\n): Effect.Effect<readonly GitOperation[], GitActivityDbError> =>\n Effect.try({\n try: () => listGitOperationsSync(filter),\n catch: (cause) => new GitActivityDbError({ operation: 'listGitOperations', cause }),\n });\n","/**\n * Git Operations Helpers (PAN-653)\n *\n * Thin wrappers over raw git commands that:\n * 1. Use execAsync (never execSync) — safe for dashboard server use\n * 2. Record before/after/remote SHAs for each operation\n * 3. Emit structured git.* events to the git_operations SQLite table\n * 4. For gitPush: perform a pre-push fetch + ancestor check and throw\n * MainDivergedError if origin/main has advanced past the local ancestor\n *\n * Usage: import { gitPush, gitFetch, gitForcePush, gitMerge, gitRevParse }\n * from 'src/lib/git/operations.js'\n *\n * NEVER add execSync, readFileSync, writeFileSync here — this module is\n * imported by dashboard server code (CLAUDE.md rule).\n */\n\nimport { execFile } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect } from 'effect';\nimport { appendGitOperationSync } from '../git-activity.js';\nimport { GitError, VcsError } from '../errors.js';\n\nconst execFileAsync = promisify(execFile);\n\n// ============== Error types ==============\n\n/**\n * Thrown by gitPush when origin/main has advanced beyond the local ancestor.\n * The caller must NOT push — it should call markWorkspaceStuck instead.\n */\nexport class MainDivergedError extends Error {\n readonly localSha: string;\n readonly remoteSha: string;\n\n constructor(localSha: string, remoteSha: string) {\n super(\n `Main diverged: remote HEAD (${remoteSha.slice(0, 7)}) is not an ancestor of local HEAD (${localSha.slice(0, 7)}). Aborting push to protect the hotfix commit.`\n );\n this.name = 'MainDivergedError';\n this.localSha = localSha;\n this.remoteSha = remoteSha;\n }\n}async function gitRevParsePromise(cwd: string, ref: string): Promise<string | null> {\n const ts = new Date().toISOString();\n try {\n const { stdout } = await execFileAsync('git', ['rev-parse', ref], { cwd, encoding: 'utf-8' });\n const sha = stdout.trim();\n appendGitOperationSync({ operation: 'rev_parse', branch: ref, issueId: undefined, afterSha: sha, status: 'success', ts });\n return sha || null;\n } catch {\n return null;\n }\n}async function gitFetchPromise(\n cwd: string,\n remote = 'origin',\n branch?: string,\n opts: { issueId?: string } = {},\n): Promise<void> {\n const ts = new Date().toISOString();\n const args = branch ? ['fetch', remote, branch] : ['fetch', remote];\n try {\n await execFileAsync('git', args, { cwd, encoding: 'utf-8', timeout: 30000 });\n appendGitOperationSync({\n operation: 'fetch',\n branch: branch ?? remote,\n issueId: opts.issueId,\n status: 'success',\n ts,\n });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n appendGitOperationSync({\n operation: 'fetch',\n branch: branch ?? remote,\n issueId: opts.issueId,\n status: 'failure',\n error: msg,\n ts,\n });\n throw err;\n }\n}async function gitPushPromise(\n cwd: string,\n remote = 'origin',\n branch = 'main',\n opts: { issueId?: string } = {},\n): Promise<void> {\n const ts = new Date().toISOString();\n\n // Step 1: record the local HEAD before the push\n const localSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? 'unknown';\n\n // Step 2: fetch latest remote state\n await Effect.runPromise(gitFetch(cwd, remote, branch, opts));\n\n // Step 3: read the remote tracking SHA\n const remoteSha = await Effect.runPromise(gitRevParse(cwd, `${remote}/${branch}`)) ?? '';\n\n // Step 4: ancestor check — is remoteSha an ancestor of localSha?\n if (remoteSha) {\n try {\n // git merge-base --is-ancestor <commit> <commit> exits 0 if true, 1 if false\n await execFileAsync('git', ['merge-base', '--is-ancestor', remoteSha, localSha], {\n cwd,\n encoding: 'utf-8',\n timeout: 10000,\n });\n } catch (err: unknown) {\n // exit code 1 = \"not an ancestor\" (true divergence); any other code is a real git error\n if ((err as { code?: number }).code !== 1) throw err;\n appendGitOperationSync({\n operation: 'main_diverged',\n branch,\n issueId: opts.issueId,\n beforeSha: localSha,\n remoteSha,\n status: 'aborted',\n error: `origin/${branch} (${remoteSha.slice(0, 7)}) is not ancestor of local HEAD (${localSha.slice(0, 7)})`,\n ts,\n });\n throw new MainDivergedError(localSha, remoteSha);\n }\n }\n\n // Step 5: push\n try {\n await execFileAsync('git', ['push', remote, branch], { cwd, encoding: 'utf-8', timeout: 60000 });\n const afterSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? localSha;\n appendGitOperationSync({\n operation: 'push',\n branch,\n issueId: opts.issueId,\n beforeSha: localSha,\n afterSha,\n remoteSha,\n status: 'success',\n ts,\n });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n appendGitOperationSync({\n operation: 'push',\n branch,\n issueId: opts.issueId,\n beforeSha: localSha,\n remoteSha,\n status: 'failure',\n error: msg,\n ts,\n });\n throw err;\n }\n}async function gitForcePushPromise(\n cwd: string,\n remote = 'origin',\n branch = 'main',\n opts: { issueId?: string; reason?: string } = {},\n): Promise<void> {\n const ts = new Date().toISOString();\n const localSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? 'unknown';\n const remoteSha = await Effect.runPromise(gitRevParse(cwd, `${remote}/${branch}`)) ?? undefined;\n\n try {\n await execFileAsync('git', ['push', '--force-with-lease', remote, branch], {\n cwd,\n encoding: 'utf-8',\n timeout: 60000,\n });\n const afterSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? localSha;\n appendGitOperationSync({\n operation: 'force_push',\n branch,\n issueId: opts.issueId,\n beforeSha: localSha,\n afterSha,\n remoteSha,\n status: 'success',\n error: opts.reason,\n ts,\n });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n appendGitOperationSync({\n operation: 'force_push',\n branch,\n issueId: opts.issueId,\n beforeSha: localSha,\n remoteSha,\n status: 'failure',\n error: msg,\n ts,\n });\n throw err;\n }\n}async function gitMergePromise(\n cwd: string,\n branch: string,\n opts: { issueId?: string; noFf?: boolean } = {},\n): Promise<void> {\n const ts = new Date().toISOString();\n const beforeSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? 'unknown';\n const args = opts.noFf ? ['merge', '--no-ff', branch] : ['merge', branch];\n try {\n await execFileAsync('git', args, { cwd, encoding: 'utf-8', timeout: 60000 });\n const afterSha = await Effect.runPromise(gitRevParse(cwd, 'HEAD')) ?? beforeSha;\n appendGitOperationSync({\n operation: 'merge',\n branch,\n issueId: opts.issueId,\n beforeSha,\n afterSha,\n status: 'success',\n ts,\n });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n appendGitOperationSync({\n operation: 'merge',\n branch,\n issueId: opts.issueId,\n beforeSha,\n status: 'failure',\n error: msg,\n ts,\n });\n throw err;\n }\n}\n\n// ─── Effect variants (PAN-1249, additive) ────────────────────────────────────\n//\n// These wrap the existing Promise-based functions with typed Effect error\n// channels (GitError / VcsError). The underlying impl is unchanged; this is\n// an additive surface for callers that already speak Effect. MainDivergedError\n// is thrown by the underlying gitPush; the Effect variant surfaces it\n// preserved as the cause inside a VcsError tagged 'main-diverged'.\n\n/** Resolve a git ref to its SHA. Returns null if the ref does not exist. */\nexport function gitRevParse(\n cwd: string,\n ref: string,\n): Effect.Effect<string | null> {\n return Effect.promise(() => gitRevParsePromise(cwd, ref));\n}\n\n/** Fetch from a remote. */\nexport function gitFetch(\n cwd: string,\n remote = 'origin',\n branch?: string,\n opts: { issueId?: string } = {},\n): Effect.Effect<void, GitError> {\n return Effect.tryPromise({\n try: () => gitFetchPromise(cwd, remote, branch, opts),\n catch: (cause) =>\n new GitError({\n command: branch ? ['git', 'fetch', remote, branch] : ['git', 'fetch', remote],\n stderr: cause instanceof Error ? cause.message : String(cause),\n exitCode: -1,\n cause,\n }),\n });\n}\n\n/**\n * Push a branch to a remote with divergence guard.\n * Fails with VcsError tagged 'main-diverged' when MainDivergedError is thrown.\n */\nexport function gitPush(\n cwd: string,\n remote = 'origin',\n branch = 'main',\n opts: { issueId?: string } = {},\n): Effect.Effect<void, VcsError | GitError> {\n return Effect.tryPromise({\n try: () => gitPushPromise(cwd, remote, branch, opts),\n catch: (cause) => {\n if (cause instanceof MainDivergedError) {\n return new VcsError({\n operation: 'main-diverged',\n message: cause.message,\n cause,\n });\n }\n return new GitError({\n command: ['git', 'push', remote, branch],\n stderr: cause instanceof Error ? cause.message : String(cause),\n exitCode: -1,\n cause,\n });\n },\n });\n}\n\n/** Force-push a branch (--force-with-lease). */\nexport function gitForcePush(\n cwd: string,\n remote = 'origin',\n branch = 'main',\n opts: { issueId?: string; reason?: string } = {},\n): Effect.Effect<void, GitError> {\n return Effect.tryPromise({\n try: () => gitForcePushPromise(cwd, remote, branch, opts),\n catch: (cause) =>\n new GitError({\n command: ['git', 'push', '--force-with-lease', remote, branch],\n stderr: cause instanceof Error ? cause.message : String(cause),\n exitCode: -1,\n cause,\n }),\n });\n}\n\n/** Merge a branch into the current branch. */\nexport function gitMerge(\n cwd: string,\n branch: string,\n opts: { issueId?: string; noFf?: boolean } = {},\n): Effect.Effect<void, GitError> {\n return Effect.tryPromise({\n try: () => gitMergePromise(cwd, branch, opts),\n catch: (cause) =>\n new GitError({\n command: opts.noFf ? ['git', 'merge', '--no-ff', branch] : ['git', 'merge', branch],\n stderr: cause instanceof Error ? cause.message : String(cause),\n exitCode: -1,\n cause,\n }),\n });\n}\n","/**\n * Merge Agent - Automatic merge conflict resolution using Claude Code\n */\n\nimport { existsSync, mkdirSync, appendFileSync } from 'fs';\nimport { writeFile } from 'fs/promises';\nimport { join, dirname, basename, relative } from 'path';\nimport { fileURLToPath } from 'url';\nimport { spawn, exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Effect } from 'effect';\nimport { capturePane, killSession, listSessionNames, sendKeys, sessionExists } from '../tmux.js';\nimport { emitActivityEntrySync, emitActivityTtsSync } from '../activity-logger.js';\nimport { loadConfigSync } from '../config-yaml.js';\n\nconst execAsync = promisify(exec);\n\n/**\n * Paths that must never enter a pipeline auto-commit, regardless of gitignore\n * state. These are workspace-local or machine-local state files and sync-target\n * directories; committing them pollutes feature branches and main.\n */\nexport const AUTO_COMMIT_EXCLUDED_PATHS = [\n '.pan/kickoff.md',\n '.pan/continue.json',\n '.pan/handoff-*.md',\n '.pan/spec.vbrief.json',\n '.claude/rules/',\n '.claude/skills/',\n // PAN-1899: machine-local Overdeck config copied into every workspace by\n // copyOverdeckSettingsToWorkspaceSync (config.yaml, projects.yaml,\n // settings.json). It diverges from main constantly and must never enter a\n // feature branch — that was the source of the recurring projects.yaml sync\n // conflict.\n '.overdeck/',\n];\n\nconst SYNC_MAIN_MAIN_PREFERRED_PATHS = [\n '.pan/continues',\n '.pan/specs',\n '.beads',\n];\n\nexport function isSyncMainMainPreferredPath(relativePath: string): boolean {\n const normalized = relativePath.replace(/\\\\/g, '/');\n return SYNC_MAIN_MAIN_PREFERRED_PATHS.some(\n (prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`),\n );\n}\n\nexport function isAutoCommitExcludedPath(relativePath: string): boolean {\n const normalized = relativePath.replace(/\\\\/g, '/');\n for (const pattern of AUTO_COMMIT_EXCLUDED_PATHS) {\n if (pattern.endsWith('/')) {\n if (normalized.startsWith(pattern) || normalized === pattern.slice(0, -1)) {\n return true;\n }\n } else if (pattern.includes('*')) {\n const regex = new RegExp(\n '^' + pattern.replace(/\\./g, '\\\\.').replace(/\\*/g, '[^/]*') + '$'\n );\n if (regex.test(normalized)) return true;\n } else if (normalized === pattern) {\n return true;\n }\n }\n return false;\n}\n\nfunction parseStatusPath(line: string): string {\n // git status --porcelain lines are \"XY PATH\" or \"XY ORIG -> DEST\" for renames.\n const body = line.slice(3);\n if (line[0] === 'R' || line[1] === 'R') {\n const parts = body.split(' -> ');\n return parts[parts.length - 1];\n }\n return body;\n}\n\n/**\n * Auto-commit non-excluded workspace changes before a sync-main merge.\n * Respects .gitignore (no -f), unstages excluded paths, and leaves excluded\n * paths dirty so the sync can proceed. Returns success=false on git errors.\n */\nexport async function autoCommitWorkspaceChangesBeforeSync(\n projectPath: string,\n): Promise<{ success: boolean; committed: boolean; reason?: string }> {\n try {\n const { stdout: statusOut } = await execAsync('git status --porcelain', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n if (!statusOut.trim()) {\n return { success: true, committed: false, reason: 'no uncommitted changes' };\n }\n\n // PAN-1819: plain `git add -A` respects .gitignore; never use -f.\n await execAsync('git add -A', { cwd: projectPath, encoding: 'utf-8' });\n\n // Belt-and-suspenders: unstage excluded paths regardless of ignore state.\n const resetPaths = AUTO_COMMIT_EXCLUDED_PATHS.map((p) =>\n p.endsWith('/') ? p.slice(0, -1) : p\n ).join(' ');\n await execAsync(`git reset HEAD -- ${resetPaths}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n\n const { stdout: diffStat } = await execAsync('git diff --cached --stat', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n if (!diffStat.trim()) {\n return { success: true, committed: false, reason: 'only excluded/ignored changes remain' };\n }\n\n await execAsync('git commit -m \"chore: auto-commit before sync with main\"', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n return { success: true, committed: true };\n } catch (error: any) {\n return { success: false, committed: false, reason: `Failed to auto-commit: ${error.message}` };\n }\n}\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\nimport {\n OVERDECK_HOME,\n} from '../paths.js';\nimport { resolveGitHubIssueSync } from '../tracker-utils.js';\n\nimport { restoreTrackedBeadsExport } from '../beads-restore.js';\nimport { runQualityGates } from './validation.js';\nimport { loadProjectsConfigSync } from '../projects.js';\nimport { cleanupStaleLocks } from '../git-utils.js';\nimport { gitPush, MainDivergedError } from '../git/operations.js';\nimport { markWorkspaceStuck, setReviewStatusSync } from '../review-status.js';\nimport { appendGitOperationSync, type GitOperationType } from '../git-activity.js';\nimport { recordFeatureRegistryLifecycle } from '../registry/feature-registry-population.js';\nimport { verifyMergedBeforeLifecycle, type PostMergeLifecycleOptions } from './merge-verification.js';\n\nconst SPECIALISTS_DIR = join(OVERDECK_HOME, 'specialists');\nconst MERGE_HISTORY_DIR = join(SPECIALISTS_DIR, 'merge-agent');\nconst MERGE_HISTORY_FILE = join(MERGE_HISTORY_DIR, 'history.jsonl');\n\n/**\n * Context for a merge conflict resolution request\n */\nexport interface MergeConflictContext {\n projectPath: string;\n sourceBranch: string;\n targetBranch: string;\n conflictFiles: string[];\n issueId: string;\n testCommand?: string;\n}\n\n/**\n * Result of merge agent execution\n */\nexport interface MergeResult {\n success: boolean;\n resolvedFiles?: string[];\n failedFiles?: string[];\n testsStatus?: 'PASS' | 'FAIL' | 'SKIP';\n validationStatus?: 'PASS' | 'FAIL' | 'NOT_RUN';\n reason?: string;\n notes?: string;\n output?: string;\n}\n\n/**\n * Merge history entry\n */\ninterface MergeHistoryEntry {\n timestamp: string;\n issueId: string;\n sourceBranch: string;\n targetBranch: string;\n conflictFiles: string[];\n result: MergeResult;\n sessionId?: string;\n}\n\n/**\n * Timeout for merge agent in milliseconds (15 minutes)\n */\n/**\n * Notify TLDR daemon to reindex changed files after merge\n */\nexport async function notifyTldrDaemon(projectPath: string, _sourceBranch: string): Promise<void> {\n try {\n console.log(`[merge-agent] Notifying TLDR daemon to reindex changed files...`);\n\n // Check if TLDR daemon is available\n const venvPath = join(projectPath, '.venv');\n if (!existsSync(venvPath)) {\n console.log(`[merge-agent] No .venv found, skipping TLDR notification`);\n return;\n }\n\n // Get changed files from the merge\n const { stdout } = await execAsync(`git diff --name-only HEAD~1 HEAD`, {\n cwd: projectPath,\n encoding: 'utf-8'\n });\n\n const changedFiles = stdout\n .trim()\n .split('\\n')\n .filter(f => f.trim().length > 0)\n .filter(f => {\n // Only include source code files (skip docs, configs, etc)\n const ext = f.split('.').pop()?.toLowerCase();\n return ext && ['ts', 'js', 'tsx', 'jsx', 'py', 'java', 'go', 'rs', 'cpp', 'c', 'h'].includes(ext);\n });\n\n if (changedFiles.length === 0) {\n console.log(`[merge-agent] No source files changed, skipping TLDR notification`);\n return;\n }\n\n console.log(`[merge-agent] Found ${changedFiles.length} changed source files to reindex`);\n\n // Get TLDR daemon service\n const { getTldrDaemonServiceSync } = await import('../tldr-daemon.js');\n const tldrService = getTldrDaemonServiceSync(projectPath, venvPath);\n\n // Check if daemon is running\n const status = await tldrService.getStatus();\n if (!status.running) {\n console.log(`[merge-agent] TLDR daemon not running, skipping notification`);\n return;\n }\n\n // Trigger warm to reindex (this will update the index incrementally)\n console.log(`[merge-agent] Triggering TLDR index warm...`);\n await tldrService.warm(true); // background mode\n\n console.log(`[merge-agent] ✓ TLDR daemon notified to reindex`);\n logActivity('tldr_notified', `Notified TLDR daemon to reindex ${changedFiles.length} files`);\n } catch (error: any) {\n // Non-fatal - log warning and continue\n console.warn(`[merge-agent] Failed to notify TLDR daemon: ${error.message}`);\n logActivity('tldr_notify_error', `TLDR notification failed: ${error.message}`);\n }\n}\n\n/**\n * Post-merge handoff: mark merged work as verifying on main and free runtime resources.\n *\n * Leaves the issue, workspace, vBRIEF, branches, and agent state dirs intact.\n * The explicit close-out ceremony performs final archival and destructive cleanup.\n *\n * IDEMPOTENT: Safe to call multiple times for the same issueId. Tracks completed\n * issues and returns immediately on re-entry. This is defense-in-depth against\n * the infinite loop that burned 24,626 Linear API calls (PAN-328).\n */\n\n// Defense-in-depth: track issues that have completed postMergeLifecycle.\n// Prevents re-execution even if caller guards fail. Persists for server lifetime.\nconst _completedPostMerge = new Set<string>();\nconst _postMergeInFlight = new Map<string, Promise<void>>();\n\n// PAN-1531: dropLingeringPreMergeStashes removed. The pre-merge stash kind\n// is no longer created by Overdeck, so there's nothing for the post-merge\n// lifecycle to clean up. Pre-existing pre-merge:* residue in refs/stash is\n// expected and inert.\n\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\"'\"'`)}'`;\n}\n\nexport async function postMergeLifecycle(\n issueId: string,\n projectPath: string,\n sourceBranch?: string,\n options?: PostMergeLifecycleOptions,\n): Promise<void> {\n // PAN-1517: the per-slot swarm runtime is gone. Slot branches no longer exist\n // — parallelism is an in-context concern owned by the work agent (see\n // roles/work.md \"Parallel work via subagents\"). postMergeLifecycle fires only\n // for the issue's main feature branch merging to `main`.\n\n // Guard 1: skip if already completed (defense-in-depth against infinite loops)\n if (_completedPostMerge.has(issueId)) {\n console.log(`[merge-agent] postMergeLifecycle already completed for ${issueId}, skipping`);\n return;\n }\n\n const inFlight = _postMergeInFlight.get(issueId);\n if (inFlight) {\n console.log(`[merge-agent] postMergeLifecycle already running for ${issueId}, joining in-flight run`);\n return inFlight;\n }\n\n const run = (async () => {\n // Guard 2: closed-out is TERMINAL. Close-out flips the spec on main to\n // completed/cancelled, clears review status, and closes the tracker issue.\n // Re-running the handoff after that resurrects the review row and REOPENS\n // the closed issue — observed live on PAN-1190 (2026-06-11): the deacon's\n // stale-mergeStatus sweep saw the cleared row as \"stale\" 47 minutes after\n // close-out and the handoff reopened it into verifying-on-main forever.\n try {\n const { findSpecByIssue } = await import('../pan-dir/specs.js');\n const spec = await Effect.runPromise(findSpecByIssue(projectPath, issueId));\n if (spec && (spec.status === 'completed' || spec.status === 'cancelled')) {\n console.log(`[merge-agent] ${issueId} is closed out (spec ${spec.status}) — skipping post-merge lifecycle`);\n _completedPostMerge.add(issueId);\n return;\n }\n } catch {\n // Spec unreadable — proceed; the guard is best-effort.\n }\n\n const mergeVerification = await verifyMergedBeforeLifecycle(issueId, projectPath, sourceBranch, options);\n if (!mergeVerification.merged) {\n console.warn(`[merge-agent] Refusing post-merge lifecycle for ${issueId}: ${mergeVerification.reason}`);\n return;\n }\n console.log(`[merge-agent] Verified merge before lifecycle for ${issueId}: ${mergeVerification.reason}`);\n\n // Set mergeStatus='merged' after verifying the branch or PR actually landed.\n try {\n setReviewStatusSync(issueId, {\n mergeStatus: 'merged',\n readyForMerge: false,\n ...(options?.markReviewPassed ? { reviewStatus: 'passed' as const } : {}),\n });\n console.log(`[merge-agent] ✓ mergeStatus set to 'merged' for ${issueId}`);\n } catch (err: any) {\n console.warn(`[merge-agent] Could not set mergeStatus: ${err.message}`);\n }\n\n // Step 0: Write pending lifecycle file and spawn detached deploy script.\n // The deploy script rebuilds dist/, kills this server, and starts a fresh process.\n // The fresh process reads the pending file on startup and runs the lifecycle steps\n // with correct module chunk references (no ERR_MODULE_NOT_FOUND after merge).\n //\n // Skip this step when we ARE the fresh process (called from processPendingLifecycle) —\n // dynamic imports already resolve correctly and spawning again would create an infinite loop.\n if (!options?.skipDeploy) {\n const pendingFile = join(OVERDECK_HOME, 'pending-post-merge.json');\n let repoRoot = __dirname.includes('/src/')\n ? __dirname.replace(/\\/src\\/.*$/, '')\n : __dirname.replace(/\\/dist\\/.*$/, '').replace(/\\/lib\\/.*$/, '');\n // If running from a workspace (workspaces/feature-*/), resolve to the main repo root.\n // Without this, the deploy script builds and npm-links from the workspace, hijacking\n // the global `pan` CLI to point at stale workspace code.\n const wsMatch = repoRoot.match(/^(.+)\\/workspaces\\/feature-[^/]+$/);\n if (wsMatch) {\n repoRoot = wsMatch[1];\n console.log(`[merge-agent] Resolved workspace repoRoot to main repo: ${repoRoot}`);\n }\n const deployScript = join(repoRoot, 'scripts', 'post-merge-deploy.sh');\n\n try {\n const pendingData = JSON.stringify({\n issueId,\n projectPath,\n sourceBranch: sourceBranch ?? '',\n timestamp: Date.now(),\n reason: 'post-merge',\n trigger: 'merge-agent',\n });\n await writeFile(pendingFile, pendingData, 'utf-8');\n console.log(`[merge-agent] Wrote pending lifecycle file: ${pendingFile}`);\n\n // Pass 'post-merge' as the reason to the deploy script so it writes the\n // restart marker. We spawn detached and return immediately — the deploy script\n // kills this server. The new server reads the pending file on boot,\n // emits lifecycle_started, and after processing emits lifecycle_complete/failed.\n const child = spawn(deployScript, [repoRoot, issueId, projectPath, sourceBranch ?? '', 'post-merge'], {\n detached: true,\n stdio: 'ignore',\n });\n child.unref();\n console.log(`[merge-agent] Spawned detached deploy script (pid ${child.pid}) — server will restart with new build`);\n return;\n } catch (err: any) {\n console.warn(`[merge-agent] Failed to spawn deploy script: ${err.message}. Falling through to in-process lifecycle (may fail on stale chunks).`);\n }\n }\n\n console.log(`[merge-agent] Running post-merge verify handoff for ${issueId}`);\n\n // 1. Clean up stale workflow labels and keep the legacy merged marker for history.\n // verifying-on-main is applied next and takes precedence in canonical state mapping.\n try {\n const { cleanupMergedLabels } = await import('../lifecycle/label-cleanup.js');\n const ghResolved = resolveGitHubIssueSync(issueId);\n const labelCtx = ghResolved.isGitHub\n ? { issueId, projectPath, github: { owner: ghResolved.owner, repo: ghResolved.repo, number: ghResolved.number } }\n : { issueId, projectPath };\n // PAN-1249: cleanupMergedLabels returns Effect<StepResult>; bridge to Promise.\n const labelResult = await Effect.runPromise(cleanupMergedLabels(labelCtx));\n if (labelResult.success && !labelResult.skipped) {\n console.log(`[merge-agent] ✓ ${labelResult.details?.join('; ')}`);\n logActivity('labels_cleaned', labelResult.details?.join('; ') || 'Labels cleaned');\n } else if (labelResult.skipped) {\n console.log(`[merge-agent] Label cleanup skipped: ${labelResult.details?.join('; ')}`);\n } else {\n console.warn(`[merge-agent] Label cleanup failed (non-fatal): ${labelResult.error}`);\n }\n } catch (err) {\n console.warn(`[merge-agent] Could not clean labels: ${err}`);\n }\n\n try {\n await transitionIssueToVerifyingOnMain(issueId, projectPath);\n void recordFeatureRegistryLifecycle({ issueId, status: 'merged' });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.warn(`[merge-agent] Could not transition issue to verifying_on_main: ${message}`);\n try {\n setReviewStatusSync(issueId, {\n mergeStatus: 'failed',\n readyForMerge: false,\n mergeNotes: `Post-merge verifying_on_main transition failed: ${message}`,\n });\n } catch (statusErr: any) {\n console.warn(`[merge-agent] Could not persist verifying_on_main transition failure: ${statusErr?.message ?? statusErr}`);\n }\n announceMerge('failed', issueId, `Post-merge verifying_on_main transition failed: ${message}`);\n logActivity('merge_failed', `Post-merge verifying_on_main transition failed for ${issueId}: ${message}`);\n throw err;\n }\n\n // 2. Compact old beads (via lifecycle module)\n try {\n const { compactBeads } = await import('../lifecycle/compact-beads.js');\n // PAN-1249: compactBeads returns Effect<StepResult>; bridge to Promise.\n const beadsResult = await Effect.runPromise(compactBeads({ issueId, projectPath }));\n if (beadsResult.success && !beadsResult.skipped) {\n console.log(`[merge-agent] ✓ ${beadsResult.details?.join('; ')}`);\n logActivity('beads_compaction_complete', beadsResult.details?.join('; ') || 'Beads compacted');\n }\n } catch (err) {\n console.warn(`[merge-agent] Beads compaction failed: ${err}`);\n }\n\n // 3. Pause work/planning agents and kill their tmux panes to free resources.\n try {\n const { setAgentPaused, getAgentState } = await import('../agents.js');\n const { killSession, sessionExists } = await import('../tmux.js');\n const issueLower = issueId.toLowerCase();\n const reason = 'awaiting close-out (verify on main)';\n for (const agentId of [`agent-${issueLower}`, `planning-${issueLower}`]) {\n // Pause, then VERIFY the gate actually persisted to state.json. A server\n // restart mid-lifecycle (the PAN-1723 deploy re-runs this from\n // pending-post-merge.json) or a concurrent deacon read-modify-write on\n // state.json can silently drop the pause. An unpaused merged work agent\n // sits idle at its prompt yet still counts against the PAN-1665 work\n // ceiling, throttling dispatch for every live issue (PAN-1726). Assert\n // paused=true after the write; retry once, then fail loudly.\n const initial = await Effect.runPromise(setAgentPaused(agentId, reason, true));\n if (initial === null) {\n // No state.json for this agent — nothing to pause (e.g. planning never ran).\n continue;\n }\n let verify = await Effect.runPromise(getAgentState(agentId));\n if (verify?.paused !== true) {\n await Effect.runPromise(setAgentPaused(agentId, reason, true));\n verify = await Effect.runPromise(getAgentState(agentId));\n }\n if (verify?.paused === true) {\n console.log(`[merge-agent] ✓ Paused ${agentId}: ${reason}`);\n } else {\n console.error(\n `[merge-agent] ✗ FAILED to persist pause for ${agentId} after merge — state.json paused=${verify?.paused}. ` +\n `Idle merged work agent may hold a PAN-1665 work slot and throttle dispatch (PAN-1726).`,\n );\n logActivity('agent_pause_failed', `Could not persist pause for ${agentId} after merge — may throttle dispatch (PAN-1726)`);\n }\n if (await Effect.runPromise(sessionExists(agentId))) {\n await Effect.runPromise(killSession(agentId));\n console.log(`[merge-agent] ✓ Killed ${agentId} tmux session to free resources`);\n logActivity('agent_session_killed', `Freed resources: killed tmux session for ${agentId}`);\n }\n }\n } catch (err) {\n console.warn(`[merge-agent] Could not pause or kill agent sessions: ${err}`);\n }\n\n // 5a. Kill canonical reviewer/synthesis sessions (PAN-915).\n // Sessions persist across review rounds to preserve reviewer context, so the\n // merge is the right moment to tear them down. Issue is done — context value\n // is zero, RSS leak risk is non-zero. Resolve projectKey from the project\n // path so we don't depend on caller-supplied config.\n try {\n const { killAllReviewerSessions } = await import('./review-agent.js');\n const { resolveProjectFromIssueSync } = await import('../projects.js');\n const resolved = resolveProjectFromIssueSync(issueId);\n const projectKey = resolved?.projectKey;\n if (projectKey) {\n const { killed } = await Effect.runPromise(killAllReviewerSessions(projectKey, issueId));\n if (killed.length > 0) {\n console.log(`[merge-agent] ✓ Killed ${killed.length} canonical reviewer session(s) for ${issueId}`);\n logActivity('reviewer_sessions_killed', `Killed ${killed.length} reviewer session(s) for ${issueId} on merge`);\n }\n }\n } catch (err) {\n console.warn(`[merge-agent] Could not kill canonical reviewer sessions: ${err}`);\n }\n\n await killPostMergeRoleSessions(issueId);\n\n // 3c. Create a workspace-scoped memory reset marker so old review-blocker noise stays archived but out of retrieval.\n try {\n const { createResetMarker } = await import('../memory/cli.js');\n const timestamp = new Date().toISOString();\n const marker = await createResetMarker({\n projectId: basename(projectPath),\n scope: 'workspace',\n scopeId: `feature-${issueId.toLowerCase()}`,\n reason: 'post-merge cleanup',\n fromTimestamp: timestamp,\n createdAt: timestamp,\n });\n console.log(`[merge-agent] ✓ Created memory reset marker ${marker.id} for ${marker.scope}:${marker.scopeId}`);\n } catch (err) {\n console.warn(`[merge-agent] Memory reset marker creation failed (non-fatal): ${err}`);\n }\n\n // 4. Stop Docker containers + networks to prevent network pool exhaustion (non-fatal)\n // Orphaned Docker networks accumulate when workspaces are merged but containers are never\n // torn down, eventually exhausting Docker's address pool and blocking new workspace creation.\n try {\n const { findWorkspacePath } = await import('../lifecycle/archive-planning.js');\n const { stopWorkspaceDocker } = await import('../workspace-manager.js');\n const issueLower = issueId.toLowerCase();\n const workspacePath = findWorkspacePath(projectPath, issueLower);\n if (workspacePath) {\n const dockerResult = await Effect.runPromise(stopWorkspaceDocker(workspacePath, issueLower));\n if (dockerResult.containersFound) {\n console.log(`[merge-agent] ✓ Stopped Docker containers: ${dockerResult.steps.join('; ')}`);\n logActivity('docker_cleanup', `Stopped Docker for ${issueId}: ${dockerResult.steps.join('; ')}`);\n }\n }\n } catch (err) {\n console.warn(`[merge-agent] Docker cleanup failed (non-fatal): ${err}`);\n }\n\n await notifyTldrDaemon(projectPath, sourceBranch ?? '');\n await maybeSpawnPostMergeKnowledgeRetro(issueId, projectPath);\n\n // Mark completed BEFORE logging — prevents re-entry even if the log line triggers something\n _completedPostMerge.add(issueId);\n\n console.log(`[merge-agent] Post-merge handoff completed for ${issueId}. Awaiting close-out (verify on main).`);\n announceMerge('completed', issueId);\n logActivity('merge_complete', `Merged ${issueId}. Awaiting close-out (verify on main).`);\n })().finally(() => {\n _postMergeInFlight.delete(issueId);\n });\n _postMergeInFlight.set(issueId, run);\n return run;\n}\n\nasync function transitionIssueToVerifyingOnMain(issueId: string, projectPath: string): Promise<void> {\n const [effectModule, issueLifecycleModule, githubModule, linearModule, rallyModule, errorsModule] = await Promise.all([\n import('effect'),\n import('../../dashboard/server/services/issue-lifecycle.js'),\n import('../../dashboard/server/services/github-client.js'),\n import('../../dashboard/server/services/linear-client.js'),\n import('../../dashboard/server/services/rally-client.js'),\n import('../../dashboard/server/services/typed-errors.js'),\n ]);\n const { Effect, Layer } = effectModule;\n const { IssueLifecycle, IssueLifecycleLive } = issueLifecycleModule;\n const { GitHubClient } = githubModule;\n const { LinearClientOptionalLive } = linearModule;\n const { RallyClientOptionalLive } = rallyModule;\n const { IssueNotFound } = errorsModule;\n const gitHubRepo = (owner: string, repo: string) => shellQuote(`${owner}/${repo}`);\n const githubLayer = Layer.succeed(GitHubClient, {\n getIssue: (_owner: string, _repo: string, number: number) => Effect.fail(new IssueNotFound({ id: String(number) })),\n closeIssue: (owner: string, repo: string, number: number) => Effect.promise(() => execAsync(`gh issue close ${number} --repo ${gitHubRepo(owner, repo)}`, { cwd: projectPath, encoding: 'utf-8' }).then(() => undefined)),\n reopenIssue: (owner: string, repo: string, number: number) => Effect.promise(() => execAsync(`gh issue reopen ${number} --repo ${gitHubRepo(owner, repo)} 2>/dev/null || true`, { cwd: projectPath, encoding: 'utf-8' }).then(() => undefined)),\n addLabel: (owner: string, repo: string, number: number, label: string) => Effect.promise(async () => {\n if (label === 'verifying-on-main') {\n await execAsync(`gh label create ${shellQuote(label)} --repo ${gitHubRepo(owner, repo)} --color \"fbca04\" --description \"Merged — awaiting verification on main\" --force 2>/dev/null || true`, { cwd: projectPath, encoding: 'utf-8' });\n }\n await execAsync(`gh issue edit ${number} --repo ${gitHubRepo(owner, repo)} --add-label ${shellQuote(label)}`, { cwd: projectPath, encoding: 'utf-8' });\n }),\n removeLabel: (owner: string, repo: string, number: number, label: string) => Effect.promise(() => execAsync(`gh issue edit ${number} --repo ${gitHubRepo(owner, repo)} --remove-label ${shellQuote(label)} 2>/dev/null || true`, { cwd: projectPath, encoding: 'utf-8' }).then(() => undefined)),\n ensureLabel: (owner: string, repo: string, label: string, color = '0075ca', description = '') => Effect.promise(async () => {\n await execAsync(`gh label create ${shellQuote(label)} --repo ${gitHubRepo(owner, repo)} --color ${shellQuote(color)} --description ${shellQuote(description)} --force 2>/dev/null || true`, { cwd: projectPath, encoding: 'utf-8' });\n return { id: 0, name: label, color };\n }),\n addComment: () => Effect.void,\n getComments: () => Effect.succeed([]),\n });\n const layer = IssueLifecycleLive.pipe(\n Layer.provide(LinearClientOptionalLive),\n Layer.provide(githubLayer),\n Layer.provide(RallyClientOptionalLive),\n );\n await Effect.runPromise(\n Effect.gen(function* () {\n const lifecycle = yield* IssueLifecycle;\n yield* lifecycle.transitionTo(issueId, 'verifying_on_main');\n }).pipe(Effect.provide(layer)),\n );\n console.log(`[merge-agent] ✓ Transitioned ${issueId} to verifying_on_main`);\n}\n\nfunction isPostMergeRoleSession(sessionName: string, issueLower: string): boolean {\n if ([`agent-${issueLower}-test`, `agent-${issueLower}-ship`, `agent-${issueLower}-merge`].includes(sessionName)) {\n return true;\n }\n if (sessionName.startsWith(`agent-${issueLower}-review-`)) {\n return true;\n }\n return sessionName.startsWith('specialist-')\n && sessionName.includes(`-${issueLower}-`)\n && /-(review|test|merge|ship)(?:-|$)/.test(sessionName);\n}\n\nasync function killPostMergeRoleSessions(issueId: string): Promise<void> {\n try {\n const issueLower = issueId.toLowerCase();\n const sessions = await Effect.runPromise(listSessionNames());\n const targets = sessions.filter((session) => isPostMergeRoleSession(session, issueLower));\n for (const session of targets) {\n await Effect.runPromise(killSession(session));\n }\n if (targets.length > 0) {\n console.log(`[merge-agent] ✓ Killed ${targets.length} review/test/ship session(s) for ${issueId}`);\n logActivity('role_sessions_killed', `Killed ${targets.length} review/test/ship session(s) for ${issueId} on merge`);\n }\n } catch (err) {\n console.warn(`[merge-agent] Could not kill role sessions for ${issueId}: ${err}`);\n }\n}\n\nfunction isPostMergeKnowledgeRetroEnabled(): boolean {\n try {\n return loadConfigSync().config.knowledge?.postMergeAutoRetro === true;\n } catch (err) {\n console.warn(`[merge-agent] Could not read knowledge post-merge retro config: ${err}`);\n return false;\n }\n}\n\nasync function maybeSpawnPostMergeKnowledgeRetro(issueId: string, projectPath: string): Promise<void> {\n if (!isPostMergeKnowledgeRetroEnabled()) return;\n\n try {\n const child = spawn('pan', ['knowledge', issueId, '--retro'], {\n cwd: projectPath,\n detached: true,\n stdio: 'ignore',\n });\n child.once?.('error', (err) => {\n const message = err instanceof Error ? err.message : String(err);\n console.warn(`[merge-agent] Post-merge knowledge retro spawn failed for ${issueId}: ${message}`);\n logActivity('knowledge_retro_error', `Post-merge knowledge retro spawn failed for ${issueId}: ${message}`, issueId);\n });\n child.unref();\n console.log(`[merge-agent] Spawned post-merge knowledge retro for ${issueId} (pid ${child.pid})`);\n logActivity('knowledge_retro_spawned', `Spawned post-merge knowledge retro for ${issueId}`, issueId);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.warn(`[merge-agent] Post-merge knowledge retro spawn failed for ${issueId}: ${message}`);\n logActivity('knowledge_retro_error', `Post-merge knowledge retro spawn failed for ${issueId}: ${message}`, issueId);\n }\n}\n\n/**\n * Reset postMergeLifecycle completion tracking for an issue (used by reopen).\n */\nexport function resetPostMergeState(issueId: string): void {\n _completedPostMerge.delete(issueId);\n _postMergeInFlight.delete(issueId);\n}\n\n/**\n * Parse result markers from agent output\n */\nexport function parseAgentOutput(output: string): MergeResult {\n const lines = output.split('\\n');\n\n let mergeResult: 'SUCCESS' | 'FAILURE' | null = null;\n let resolvedFiles: string[] = [];\n let failedFiles: string[] = [];\n let testsStatus: 'PASS' | 'FAIL' | 'SKIP' | null = null;\n let validationStatus: 'PASS' | 'FAIL' | null = null;\n let reason = '';\n let notes = '';\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Match MERGE_RESULT\n if (trimmed.startsWith('MERGE_RESULT:')) {\n const value = trimmed.substring('MERGE_RESULT:'.length).trim();\n if (value === 'SUCCESS' || value === 'FAILURE') {\n mergeResult = value;\n }\n }\n\n // Match RESOLVED_FILES\n if (trimmed.startsWith('RESOLVED_FILES:')) {\n const value = trimmed.substring('RESOLVED_FILES:'.length).trim();\n resolvedFiles = value\n .split(',')\n .map((f) => f.trim())\n .filter((f) => f.length > 0);\n }\n\n // Match FAILED_FILES\n if (trimmed.startsWith('FAILED_FILES:')) {\n const value = trimmed.substring('FAILED_FILES:'.length).trim();\n failedFiles = value\n .split(',')\n .map((f) => f.trim())\n .filter((f) => f.length > 0);\n }\n\n // Match TESTS\n if (trimmed.startsWith('TESTS:')) {\n const value = trimmed.substring('TESTS:'.length).trim();\n if (value === 'PASS' || value === 'FAIL' || value === 'SKIP') {\n testsStatus = value;\n }\n }\n\n // Match VALIDATION\n if (trimmed.startsWith('VALIDATION:')) {\n const value = trimmed.substring('VALIDATION:'.length).trim();\n if (value === 'PASS' || value === 'FAIL') {\n validationStatus = value;\n }\n }\n\n // Match REASON\n if (trimmed.startsWith('REASON:')) {\n reason = trimmed.substring('REASON:'.length).trim();\n }\n\n // Match NOTES\n if (trimmed.startsWith('NOTES:')) {\n notes = trimmed.substring('NOTES:'.length).trim();\n }\n }\n\n // Build result\n if (mergeResult === 'SUCCESS') {\n return {\n success: true,\n resolvedFiles,\n testsStatus: testsStatus || 'SKIP',\n validationStatus: validationStatus || 'NOT_RUN',\n notes,\n output,\n };\n } else if (mergeResult === 'FAILURE') {\n return {\n success: false,\n failedFiles,\n validationStatus: validationStatus || 'NOT_RUN',\n reason,\n notes,\n output,\n };\n } else {\n // No structured result markers found - try to detect human-readable format\n // Agents sometimes output \"MERGE TASK COMPLETE\" instead of \"MERGE_RESULT: SUCCESS\"\n const lowerOutput = output.toLowerCase();\n\n // Check for success indicators\n const successIndicators = [\n 'merge task complete',\n 'successfully merged',\n 'merge complete',\n 'pushed merge commit',\n 'successfully merged and pushed',\n ];\n\n const failureIndicators = [\n 'merge failed',\n 'merge task failed',\n 'could not merge',\n 'conflict not resolved',\n ];\n\n const hasSuccessIndicator = successIndicators.some(i => lowerOutput.includes(i));\n const hasFailureIndicator = failureIndicators.some(i => lowerOutput.includes(i));\n\n if (hasSuccessIndicator && !hasFailureIndicator) {\n // Extract test status from output if mentioned\n let detectedTestStatus: 'PASS' | 'FAIL' | 'SKIP' = 'SKIP';\n if (lowerOutput.includes('tests: pass') || lowerOutput.includes('tests passed') ||\n output.match(/\\d+ passed/)) {\n detectedTestStatus = 'PASS';\n } else if (lowerOutput.includes('tests: fail') || lowerOutput.includes('tests failed')) {\n detectedTestStatus = 'FAIL';\n }\n\n console.log('[merge-agent] Detected success from human-readable output');\n return {\n success: true,\n testsStatus: detectedTestStatus,\n validationStatus: 'PASS',\n notes: 'Detected from human-readable output (agent did not use structured format)',\n output,\n };\n }\n\n if (hasFailureIndicator) {\n console.log('[merge-agent] Detected failure from human-readable output');\n return {\n success: false,\n validationStatus: 'NOT_RUN',\n reason: 'Detected merge failure from agent output',\n output,\n };\n }\n\n // Truly unrecognized output\n return {\n success: false,\n validationStatus: 'NOT_RUN',\n reason: 'Agent did not report result in expected format',\n output,\n };\n }\n}\n\n/**\n * Get conflict files from git status (async)\n */\nasync function getConflictFiles(projectPath: string): Promise<string[]> {\n try {\n const { stdout: status } = await execAsync('git diff --name-only --diff-filter=U', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n\n return status\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n } catch (error) {\n console.error('Failed to get conflict files:', error);\n return [];\n }\n}\n\nasync function resolveMainPreferredSyncConflicts(\n projectPath: string,\n conflictFiles: string[],\n): Promise<{ success: boolean; reason?: string }> {\n if (conflictFiles.length === 0 || !conflictFiles.every(isSyncMainMainPreferredPath)) {\n return { success: false, reason: 'conflicts include non-pipeline-owned files' };\n }\n\n try {\n for (const path of SYNC_MAIN_MAIN_PREFERRED_PATHS) {\n await execAsync(`git rm -r --quiet --ignore-unmatch -- ${path}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n await execAsync(`git checkout origin/main -- ${path}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n }).catch(() => {\n // The path may not exist on origin/main. In that case, the preceding\n // git rm records main's deletion for this pipeline-owned path.\n });\n await execAsync(`git add -A -- ${path}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n }\n\n const remainingConflicts = await getConflictFiles(projectPath);\n if (remainingConflicts.length > 0) {\n return {\n success: false,\n reason: `Unresolved conflicts remain: ${remainingConflicts.join(', ')}`,\n };\n }\n\n await execAsync('git commit --no-edit', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n return { success: true };\n } catch (error: any) {\n return {\n success: false,\n reason: `Failed to auto-resolve pipeline-owned conflicts: ${error.message}`,\n };\n }\n}\n\n/**\n * Log merge to history\n */\nexport function logMergeHistory(context: MergeConflictContext, result: MergeResult, sessionId?: string): void {\n // Ensure history directory exists\n if (!existsSync(MERGE_HISTORY_DIR)) {\n mkdirSync(MERGE_HISTORY_DIR, { recursive: true });\n }\n\n const entry: MergeHistoryEntry = {\n timestamp: new Date().toISOString(),\n issueId: context.issueId,\n sourceBranch: context.sourceBranch,\n targetBranch: context.targetBranch,\n conflictFiles: context.conflictFiles,\n result: {\n ...result,\n output: undefined, // Don't store full output in history\n },\n sessionId,\n };\n\n appendFileSync(MERGE_HISTORY_FILE, JSON.stringify(entry) + '\\n', 'utf-8');\n}\n\n/**\n * Log activity to the dashboard activity log (event-sourced via emitActivityEntry)\n */\nfunction logActivity(action: string, details: string, issueId?: string): void {\n emitActivityEntrySync({\n source: 'ship',\n level: action.includes('fail') || action.includes('error') ? 'error' : action.includes('warn') ? 'warn' : 'success',\n message: details,\n issueId,\n });\n}\n\n/**\n * Voice-worthy merge milestone announcement. Messages start with one of three\n * distinctive prefixes (\"Merge started\", \"Merge completed\", \"Merge failed\") so\n * pan-tts can filter merge-agent chatter and speak only these three events.\n *\n * Do not change these prefixes without updating the pan-tts filter\n * (~/Projects/pan-tts/src/pan_tts/__main__.py — ALLOWED_MERGE_PREFIXES).\n */\nfunction announceMerge(\n status: 'started' | 'completed' | 'failed',\n issueId: string,\n extra?: string,\n): void {\n const prefix = status === 'started'\n ? 'Merge started'\n : status === 'completed'\n ? 'Merge completed'\n : 'Merge failed';\n const tail = extra ? `. ${extra}` : '';\n emitActivityEntrySync({\n source: 'ship',\n level: status === 'failed' ? 'error' : 'success',\n message: `${prefix} for ${issueId}${tail}`,\n issueId,\n });\n // Upleveled TTS utterance — short, speakable, no issue prefix noise\n const ttsUtterance = status === 'started'\n ? `Starting merge for ${issueId}`\n : status === 'completed'\n ? `${issueId} merged to main`\n : `Merge failed for ${issueId}`;\n emitActivityTtsSync({\n utterance: ttsUtterance,\n priority: status === 'failed' ? 0 : 1,\n issueId,\n source: 'merge-agent',\n eventType: `mergeStatus.${status === 'completed' ? 'merged' : status === 'started' ? 'merging' : 'failed'}`,\n });\n}\n\n/**\n * Capture tmux output and look for result markers (async)\n */\nexport async function captureTmuxOutput(sessionName: string): Promise<string> {\n try {\n return await Effect.runPromise(capturePane(sessionName));\n } catch {\n return '';\n }\n}\n\n/** Patterns to match in tmux capture-pane output (git push/fetch lines) */\nexport const GIT_PATTERNS: Array<{ re: RegExp; operation: GitOperationType; level: 'info' | 'warn' | 'error' }> = [\n { re: /force-with-lease/i, operation: 'force_push_cmd', level: 'warn' },\n { re: /git push/i, operation: 'push_attempt', level: 'info' },\n { re: /git fetch/i, operation: 'fetch_attempt', level: 'info' },\n { re: /\\[rejected\\]/i, operation: 'push_rejected', level: 'error' },\n { re: /non-fast-forward/i, operation: 'non_ff', level: 'error' },\n { re: /retrying/i, operation: 'retry', level: 'warn' },\n { re: /\\[remote rejected\\]/i, operation: 'remote_rejected', level: 'error' },\n { re: /Everything up-to-date/i, operation: 'push_noop', level: 'info' },\n];\n\n/**\n * Scan tmux capture-pane output for git push/fetch patterns and emit each\n * as a git_operations row. Uses seenLineHashes to dedupe within a session.\n */\nexport function scanGitPatterns(\n output: string,\n seenLineHashes: Set<string>,\n issueId: string,\n branch?: string,\n): void {\n const lines = output.split('\\n');\n const ts = new Date().toISOString();\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n // Simple hash: first 120 chars (avoids hashing megabytes)\n const hash = trimmed.slice(0, 120);\n if (seenLineHashes.has(hash)) continue;\n\n for (const { re, operation, level } of GIT_PATTERNS) {\n if (re.test(trimmed)) {\n seenLineHashes.add(hash);\n appendGitOperationSync({\n operation,\n branch,\n issueId,\n status: level === 'error' ? 'failure' : 'success',\n error: level !== 'info' ? trimmed.slice(0, 200) : undefined,\n ts,\n });\n emitActivityEntrySync({\n source: 'ship',\n level,\n message: `[git] ${trimmed.slice(0, 100)}`,\n issueId,\n });\n break; // only match one pattern per line\n }\n }\n }\n}\n\n/**\n * Check if specialist-merge-agent tmux session is running (async)\n */\nexport async function isMergeAgentRunning(): Promise<boolean> {\n return Effect.runPromise(sessionExists('specialist-merge-agent'));\n}\n\n/**\n * Send a message to an agent's tmux session (async)\n */\nexport async function sendMessageToAgent(issueId: string, message: string): Promise<boolean> {\n // Agent sessions are typically named agent-{issueId} (lowercase)\n const sessionName = `agent-${issueId.toLowerCase()}`;\n\n try {\n // Check if session exists\n if (!await Effect.runPromise(sessionExists(sessionName))) {\n console.log(`[merge-agent] Could not send message to ${sessionName} (session does not exist)`);\n return false;\n }\n\n // Send the message using centralized sendKeys\n await Effect.runPromise(sendKeys(sessionName, message));\n\n console.log(`[merge-agent] Sent message to ${sessionName}`);\n logActivity('agent_message', `Sent to ${sessionName}: ${message.slice(0, 100)}...`);\n return true;\n } catch {\n console.log(`[merge-agent] Could not send message to ${sessionName} (session may not exist)`);\n return false;\n }\n}\n\n// PAN-1531: ship-role machinery (buildShipPreparationPrompt, buildShipSyncMainPrompt,\n// spawnShipRoleForTask, spawnMergeAgentForBranches, spawnRebaseAgentForBranch,\n// defaultWorkspaceForIssue) removed. Rebase is now performed in-process via\n// rebaseFeatureBranch() in src/lib/cloister/merge-rebase.ts. See docs/MERGE-WORKFLOW.md.\n\nexport async function salvageStrandedMerge(\n projectPath: string,\n targetBranch: string,\n headBefore: string,\n issueId: string,\n logActivity: (action: string, detail: string) => void,\n): Promise<{ success: boolean; reason?: string } | null> {\n try {\n const { stdout: currentHeadRaw } = await execAsync('git rev-parse HEAD', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n const currentHead = currentHeadRaw.trim();\n\n if (currentHead === headBefore) {\n // No local merge happened — nothing to salvage\n return null;\n }\n\n // Local HEAD changed — check if it's ahead of remote\n await execAsync(`git fetch origin ${targetBranch}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n timeout: 10000,\n }).catch(() => {});\n\n const { stdout: remoteHeadRaw } = await execAsync(`git rev-parse origin/${targetBranch}`, {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n\n if (remoteHeadRaw.trim() === currentHead) {\n // Already pushed (maybe by another process)\n console.log(`[merge-agent] Salvage check: merge already pushed`);\n return { success: true };\n }\n\n // Stranded merge detected — push it (with divergence guard to protect hotfixes)\n console.log(`[merge-agent] SALVAGING stranded merge for ${issueId}: local HEAD ${currentHead.slice(0, 8)} != remote ${remoteHeadRaw.trim().slice(0, 8)}`);\n logActivity('merge_salvage', `Pushing stranded merge commit ${currentHead.slice(0, 8)} for ${issueId}`);\n\n try {\n await Effect.runPromise(gitPush(projectPath, 'origin', targetBranch, { issueId }));\n } catch (pushErr: unknown) {\n if (pushErr instanceof MainDivergedError) {\n // origin has advanced past our local ancestor — a hotfix landed.\n // Mark stuck so Deacon won't re-trigger, then let the caller handle it.\n markWorkspaceStuck(issueId, 'main_diverged', {\n localSha: pushErr.localSha,\n remoteSha: pushErr.remoteSha,\n });\n logActivity('merge_salvage_diverged', `Salvage aborted: origin/${targetBranch} diverged (remote ${pushErr.remoteSha.slice(0, 7)} not ancestor of local ${pushErr.localSha.slice(0, 7)})`);\n return { success: false, reason: pushErr.message };\n }\n throw pushErr;\n }\n\n console.log(`[merge-agent] Salvage push successful for ${issueId}`);\n logActivity('merge_salvage_success', `Stranded merge pushed successfully`);\n return { success: true };\n } catch (error: any) {\n console.error(`[merge-agent] Salvage failed: ${error.message}`);\n logActivity('merge_salvage_failed', `Salvage push failed: ${error.message}`);\n return null;\n }\n}\n\n/**\n * Result of syncing main into a workspace branch\n */\nexport interface SyncMainResult {\n success: boolean;\n alreadyUpToDate?: boolean;\n commitCount?: number;\n changedFiles?: string[];\n conflictFiles?: string[];\n reason?: string;\n}\n\n/**\n * Scan workspace for leftover git conflict markers (async)\n */\nexport async function scanForConflictMarkers(projectPath: string): Promise<string[]> {\n try {\n // git diff --check exits non-zero and prints filenames when conflict markers exist\n const { stdout } = await execAsync('git diff --check 2>&1 || true', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n const files = stdout\n .split('\\n')\n .filter(line => line.includes('leftover conflict marker'))\n .map(line => line.split(':')[0].trim())\n .filter(f => f.length > 0);\n return [...new Set(files)];\n } catch {\n return [];\n }\n}\n\n/**\n * Sync the latest main branch into a workspace's feature branch.\n *\n * This performs a `git merge origin/main` in the workspace. If the merge is clean\n * it returns immediately. If conflicts arise, the conflict details are surfaced\n * for manual workspace resolution. The merge is never pushed — this is a local\n * workspace operation.\n *\n * Auto-commits any uncommitted changes before merging (with safety verification).\n */\nexport async function syncMainIntoWorkspace(\n projectPath: string,\n issueId: string,\n): Promise<SyncMainResult> {\n console.log(`[sync-main] Starting sync of main into workspace for ${issueId}`);\n logActivity('sync_main_start', `Starting sync for ${issueId}`);\n\n // PAN-1158 safety net: a workspace bd dolt DB that briefly went empty can\n // leave `.beads/issues.jsonl` reported as deleted by `git status`. The\n // auto-commit below would then propagate that deletion onto the feature\n // branch. Restore the tracked export first so the auto-commit only sees\n // intentional changes.\n await Effect.runPromise(restoreTrackedBeadsExport(projectPath));\n\n // Pre-flight: auto-commit uncommitted changes before merge\n console.log(`[sync-main] Checking for uncommitted changes...`);\n logActivity('sync_main_auto_commit', `Auto-committing uncommitted changes before sync`);\n const autoCommit = await autoCommitWorkspaceChangesBeforeSync(projectPath);\n if (!autoCommit.success) {\n const message = autoCommit.reason || 'Failed to auto-commit uncommitted changes';\n console.error(`[sync-main] ${message}`);\n logActivity('sync_main_blocked', message);\n return { success: false, reason: message };\n }\n if (autoCommit.committed) {\n console.log(`[sync-main] Auto-commit successful`);\n }\n\n // Verify no non-excluded uncommitted changes remain.\n try {\n const { stdout: postCommitStatus } = await execAsync('git status --porcelain', {\n cwd: projectPath,\n encoding: 'utf-8',\n });\n if (postCommitStatus.trim()) {\n const remainingNonExcluded = postCommitStatus\n .trim()\n .split('\\n')\n .filter((line) => !isAutoCommitExcludedPath(parseStatusPath(line)));\n if (remainingNonExcluded.length > 0) {\n const message = 'Uncommitted changes remain after auto-commit — aborting sync';\n console.error(`[sync-main] ${message}`);\n logActivity('sync_main_blocked', message);\n return { success: false, reason: message };\n }\n }\n } catch (error: any) {\n return { success: false, reason: `Failed to check git status: ${error.message}` };\n }\n\n // Clean up stale git lock files\n try {\n const lockCleanup = await Effect.runPromise(cleanupStaleLocks(projectPath));\n if (lockCleanup.found.length > 0) {\n console.log(`[sync-main] Found ${lockCleanup.found.length} lock file(s)`);\n if (lockCleanup.removed.length > 0) {\n console.log(`[sync-main] Cleaned up ${lockCleanup.removed.length} stale lock file(s)`);\n logActivity('git_lock_cleanup', `Removed ${lockCleanup.removed.length} stale lock file(s)`);\n }\n if (lockCleanup.errors.some((e: { file: string; error: string }) => e.error.includes('Git processes are running'))) {\n const message = 'Git processes are still running — cannot safely start sync';\n console.error(`[sync-main] ${message}`);\n logActivity('sync_main_blocked', message);\n return { success: false, reason: message };\n }\n }\n } catch (lockErr: any) {\n console.warn(`[sync-main] Lock cleanup warning: ${lockErr.message} (continuing)`);\n }\n\n // Fetch latest main\n try {\n console.log(`[sync-main] Fetching origin/main...`);\n await execAsync('git fetch origin main', { cwd: projectPath, encoding: 'utf-8' });\n } catch (error: any) {\n return { success: false, reason: `Failed to fetch origin/main: ${error.message}` };\n }\n\n // Attempt the merge\n let mergeOutput = '';\n let hasConflicts = false;\n try {\n const result = await execAsync('git merge origin/main', { cwd: projectPath, encoding: 'utf-8' });\n mergeOutput = (result.stdout || '') + (result.stderr || '');\n } catch (error: any) {\n mergeOutput = (error.stdout || '') + (error.stderr || '');\n hasConflicts = true;\n }\n\n // Already up to date?\n if (mergeOutput.includes('Already up to date') || mergeOutput.includes('Already up-to-date')) {\n console.log(`[sync-main] Already up to date`);\n logActivity('sync_main_noop', `${issueId} already up to date with main`);\n return { success: true, alreadyUpToDate: true };\n }\n\n if (!hasConflicts) {\n // Clean merge — collect stats\n console.log(`[sync-main] Clean merge completed`);\n logActivity('sync_main_success', `Clean merge of main into ${issueId}`);\n\n let changedFiles: string[] = [];\n let commitCount = 0;\n try {\n const { stdout: diffFiles } = await execAsync(\n 'git diff --name-only ORIG_HEAD HEAD 2>/dev/null || git diff --name-only HEAD~1 HEAD',\n { cwd: projectPath, encoding: 'utf-8' },\n );\n changedFiles = diffFiles.trim().split('\\n').filter(f => f.length > 0);\n } catch { /* non-fatal */ }\n try {\n const { stdout: logOut } = await execAsync(\n 'git log ORIG_HEAD..HEAD --oneline 2>/dev/null || echo \"\"',\n { cwd: projectPath, encoding: 'utf-8' },\n );\n commitCount = logOut.trim().split('\\n').filter(l => l.length > 0).length;\n } catch { /* non-fatal */ }\n\n return { success: true, commitCount, changedFiles };\n }\n\n // PAN-1531: sync-main conflict case — surface to operator instead of\n // delegating to an LLM ship role. Abort the merge so the working tree is\n // clean, then return the conflict files for the caller to display.\n const conflictFiles = await getConflictFiles(projectPath);\n const mainPreferredResolution = await resolveMainPreferredSyncConflicts(projectPath, conflictFiles);\n if (mainPreferredResolution.success) {\n console.log(`[sync-main] Auto-resolved ${conflictFiles.length} pipeline-owned conflict(s) with origin/main`);\n logActivity('sync_main_auto_resolved_conflicts', `Auto-resolved ${conflictFiles.length} pipeline-owned conflict(s) in ${issueId} with origin/main`);\n\n let changedFiles: string[] = [];\n let commitCount = 0;\n try {\n const { stdout: diffFiles } = await execAsync(\n 'git diff --name-only ORIG_HEAD HEAD 2>/dev/null || git diff --name-only HEAD~1 HEAD',\n { cwd: projectPath, encoding: 'utf-8' },\n );\n changedFiles = diffFiles.trim().split('\\n').filter(f => f.length > 0);\n } catch { /* non-fatal */ }\n try {\n const { stdout: logOut } = await execAsync(\n 'git log ORIG_HEAD..HEAD --oneline 2>/dev/null || echo \"\"',\n { cwd: projectPath, encoding: 'utf-8' },\n );\n commitCount = logOut.trim().split('\\n').filter(l => l.length > 0).length;\n } catch { /* non-fatal */ }\n\n return { success: true, commitCount, changedFiles };\n }\n\n console.log(`[sync-main] ${conflictFiles.length} conflict(s); aborting merge for manual resolution`);\n logActivity('sync_main_conflicts', `${conflictFiles.length} conflict(s) in ${issueId}: ${conflictFiles.join(', ')}`);\n\n try { await execAsync('git merge --abort', { cwd: projectPath, encoding: 'utf-8' }); } catch { /* non-fatal */ }\n\n return {\n success: false,\n conflictFiles,\n reason: `Sync-main produced ${conflictFiles.length} conflict(s) in ${issueId}: ${conflictFiles.join(', ')}. Resolve manually in the workspace, then re-run sync-main.`,\n };\n\n}\n\n/**\n * Look up and run quality gates for the project at projectPath.\n * Returns empty array if no quality gates are configured.\n *\n * In polyrepo mode (projectPath is a sub-repo of project.path), only gates\n * whose `path` field matches the relative sub-repo path are run. Gates with\n * no `path` field are skipped in polyrepo context.\n */\nexport async function runProjectQualityGates(\n projectPath: string,\n phase: 'pre_push' | 'post_push'\n): Promise<import('./validation.js').QualityGateResult[]> {\n try {\n const config = loadProjectsConfigSync();\n // Find the project whose path matches\n const project = Object.values(config.projects).find(p => projectPath.startsWith(p.path));\n if (!project?.quality_gates || Object.keys(project.quality_gates).length === 0) {\n console.log(`[merge-agent] No quality gates configured for ${projectPath}`);\n return [];\n }\n\n // Detect polyrepo context: if projectPath is a subdirectory of project.path,\n // repoRelPath is non-empty (e.g., 'frontend' or 'backend').\n const repoRelPath = relative(project.path, projectPath);\n const matchedRepo = project.workspace?.repos?.find(repo => (\n repoRelPath === repo.path ||\n projectPath === join(project.path, repo.path)\n ));\n const repoIdentifiers = matchedRepo\n ? new Set([matchedRepo.path, matchedRepo.name])\n : new Set(repoRelPath && !repoRelPath.startsWith('..') ? [repoRelPath] : []);\n\n let gatesToRun = project.quality_gates;\n if (repoRelPath && !repoRelPath.startsWith('..')) {\n // Polyrepo: gates can target either the repo path (\"frontend\") or the\n // configured repo key/alias (\"fe\"). Both map to the same sub-repo.\n const filtered = Object.entries(project.quality_gates).filter(\n ([, gate]) => gate.path && repoIdentifiers.has(gate.path)\n );\n if (filtered.length === 0) {\n console.log(`[merge-agent] No quality gates configured for repo path \"${repoRelPath}\"`);\n return [];\n }\n gatesToRun = Object.fromEntries(filtered);\n console.log(\n `[merge-agent] Polyrepo: running ${Object.keys(gatesToRun).length} gate(s) for path \"${repoRelPath}\" (${[...repoIdentifiers].join(', ')})`\n );\n }\n\n console.log(`[merge-agent] Running ${phase} quality gates for project \"${project.name}\"`);\n return await Effect.runPromise(runQualityGates(gatesToRun, projectPath, phase));\n } catch (error: any) {\n console.error(`[merge-agent] Failed to load quality gates: ${error.message}`);\n return [];\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { c as resetPostMergeState, r as init_merge_agent, s as postMergeLifecycle } from "./merge-agent-
|
|
1
|
+
import { c as resetPostMergeState, r as init_merge_agent, s as postMergeLifecycle } from "./merge-agent-B9hIZ6Z0.js";
|
|
2
2
|
init_merge_agent();
|
|
3
3
|
export { postMergeLifecycle, resetPostMergeState };
|