@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.
Files changed (219) hide show
  1. package/dist/{agent-state-KVbJEG5f.js → agent-state-RLhS7ckO.js} +1 -1
  2. package/dist/{agents-pp8FO7Uj.js → agents-By3u_fQ8.js} +1 -1
  3. package/dist/{agents-CQHK0CSA.js → agents-CgVr77Qq.js} +12 -12
  4. package/dist/{agents-CQHK0CSA.js.map → agents-CgVr77Qq.js.map} +1 -1
  5. package/dist/{backlog-auto-trigger-CyuoS0nl.js → backlog-auto-trigger-B_-1tk4e.js} +5 -5
  6. package/dist/{backlog-auto-trigger-CyuoS0nl.js.map → backlog-auto-trigger-B_-1tk4e.js.map} +1 -1
  7. package/dist/cli/index.js +45 -45
  8. package/dist/{cli-Cewlu3yU.js → cli-DGeSW85B.js} +2 -2
  9. package/dist/{cli-Cewlu3yU.js.map → cli-DGeSW85B.js.map} +1 -1
  10. package/dist/cli-MTB9IOoR.js +3 -0
  11. package/dist/{close-issue-BJGX9_z7.js → close-issue-Djajb6Sv.js} +2 -2
  12. package/dist/{close-issue-BJGX9_z7.js.map → close-issue-Djajb6Sv.js.map} +1 -1
  13. package/dist/{concurrency-Ca-fC7tA.js → concurrency-4rzjHAQA.js} +1 -1
  14. package/dist/{conversation-compaction-B_6s9EvM.js → conversation-compaction-BQvZh09I.js} +4 -4
  15. package/dist/{conversation-compaction-B_6s9EvM.js.map → conversation-compaction-BQvZh09I.js.map} +1 -1
  16. package/dist/dashboard/{advancing-selfheal-CMlGznKv.js → advancing-selfheal-BezNfM3P.js} +3 -3
  17. package/dist/dashboard/{advancing-selfheal-CMlGznKv.js.map → advancing-selfheal-BezNfM3P.js.map} +1 -1
  18. package/dist/dashboard/{agent-state-xk_G8t90.js → agent-state-BUANcaA7.js} +1 -1
  19. package/dist/dashboard/{agent-state-service-C1iUF9Ft.js → agent-state-service-DmFpIMbP.js} +1 -1
  20. package/dist/dashboard/{agent-state-service-5ge9xxhx.js → agent-state-service-tXUdp6UF.js} +2 -2
  21. package/dist/dashboard/{agent-state-service-5ge9xxhx.js.map → agent-state-service-tXUdp6UF.js.map} +1 -1
  22. package/dist/dashboard/{agents-BIggLvi_.js → agents-C-vqmKDi.js} +1 -1
  23. package/dist/dashboard/{agents-DHvsjsuP.js → agents-C7V4odQ6.js} +11 -11
  24. package/dist/dashboard/{agents-DHvsjsuP.js.map → agents-C7V4odQ6.js.map} +1 -1
  25. package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js → backlog-auto-trigger-Cb8PR-5w.js} +2 -2
  26. package/dist/dashboard/{backlog-auto-trigger-COm_R96V.js.map → backlog-auto-trigger-Cb8PR-5w.js.map} +1 -1
  27. package/dist/dashboard/{cli-CvK9xuSS.js → cli-BgzbB6h3.js} +2 -2
  28. package/dist/dashboard/{cli-CvK9xuSS.js.map → cli-BgzbB6h3.js.map} +1 -1
  29. package/dist/dashboard/cli-DCqy_wFa.js +3 -0
  30. package/dist/dashboard/{close-issue-BnpErnD1.js → close-issue-BWf3pxn4.js} +2 -2
  31. package/dist/dashboard/{close-issue-BnpErnD1.js.map → close-issue-BWf3pxn4.js.map} +1 -1
  32. package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js → closed-issue-reaper-D7XPq5pY.js} +4 -4
  33. package/dist/dashboard/{closed-issue-reaper-DRqK5k39.js.map → closed-issue-reaper-D7XPq5pY.js.map} +1 -1
  34. package/dist/dashboard/{closed-issue-reaper-BLCQwTpF.js → closed-issue-reaper-VSTSTROK.js} +1 -1
  35. package/dist/dashboard/{conflict-gate-JdFuhKuF.js → conflict-gate-DwOslTij.js} +1 -1
  36. package/dist/dashboard/{conflict-gate-BhKvrPxn.js → conflict-gate-FFEvTDVA.js} +3 -3
  37. package/dist/dashboard/{conflict-gate-BhKvrPxn.js.map → conflict-gate-FFEvTDVA.js.map} +1 -1
  38. package/dist/dashboard/{conversation-compaction-C5o_eKEm.js → conversation-compaction-CCqN4hWk.js} +4 -4
  39. package/dist/dashboard/{conversation-compaction-C5o_eKEm.js.map → conversation-compaction-CCqN4hWk.js.map} +1 -1
  40. package/dist/dashboard/{conversation-compaction-Ba5aZdrb.js → conversation-compaction-CL5Ti4j4.js} +1 -1
  41. package/dist/dashboard/{deacon-j3qSZ7--.js → deacon-CGHl90eC.js} +107 -50
  42. package/dist/dashboard/{deacon-j3qSZ7--.js.map → deacon-CGHl90eC.js.map} +1 -1
  43. package/dist/dashboard/{deacon-Cv2RMfHs.js → deacon-DMPh40Im.js} +2 -2
  44. package/dist/dashboard/deacon-swarm-CVTiNL0N.js +2 -0
  45. package/dist/dashboard/{deacon-swarm-DadWNi5J.js → deacon-swarm-aMMJTq6t.js} +4 -4
  46. package/dist/dashboard/{deacon-swarm-DadWNi5J.js.map → deacon-swarm-aMMJTq6t.js.map} +1 -1
  47. package/dist/dashboard/deacon.js +4 -4
  48. package/dist/dashboard/{feedback-target-oE4-Esxl.js → feedback-target-dJjFfITP.js} +4 -4
  49. package/dist/dashboard/{feedback-target-oE4-Esxl.js.map → feedback-target-dJjFfITP.js.map} +1 -1
  50. package/dist/dashboard/{feedback-target-C4DaDSZ8.js → feedback-target-nQSM6ygL.js} +1 -1
  51. package/dist/dashboard/{flywheel-ClMjGW_u.js → flywheel-BLoVNtJV.js} +6 -6
  52. package/dist/dashboard/{flywheel-ClMjGW_u.js.map → flywheel-BLoVNtJV.js.map} +1 -1
  53. package/dist/dashboard/{flywheel-DefHRzY4.js → flywheel-Cqf8vu7N.js} +6 -6
  54. package/dist/dashboard/{flywheel-DefHRzY4.js.map → flywheel-Cqf8vu7N.js.map} +1 -1
  55. package/dist/dashboard/{flywheel-BqDdGvrQ.js → flywheel-PJsbrXNY.js} +1 -1
  56. package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js → flywheel-merge-order-B7BH8mZ3.js} +2 -2
  57. package/dist/dashboard/{flywheel-merge-order-CuDcqTRJ.js.map → flywheel-merge-order-B7BH8mZ3.js.map} +1 -1
  58. package/dist/dashboard/{flywheel-merge-order-B6YDSElh.js → flywheel-merge-order-CCwF60b-.js} +1 -1
  59. package/dist/dashboard/{harness-resolve-DKeSVsJL.js → harness-resolve-B3f9G1Kp.js} +2 -2
  60. package/dist/dashboard/{harness-resolve-DKeSVsJL.js.map → harness-resolve-B3f9G1Kp.js.map} +1 -1
  61. package/dist/dashboard/{inspect-agent-BKKc0XnB.js → inspect-agent-CKF5-SJr.js} +3 -3
  62. package/dist/dashboard/{inspect-agent-BKKc0XnB.js.map → inspect-agent-CKF5-SJr.js.map} +1 -1
  63. package/dist/dashboard/inspect-agent-CqpEWGWy.js +2 -0
  64. package/dist/dashboard/{issue-lifecycle-DnhQgddB.js → issue-lifecycle-5LbOcATP.js} +7 -7
  65. package/dist/dashboard/{issue-lifecycle-DnhQgddB.js.map → issue-lifecycle-5LbOcATP.js.map} +1 -1
  66. package/dist/dashboard/{issue-lifecycle-nY4esZn8.js → issue-lifecycle-CjYqQ669.js} +1 -1
  67. package/dist/dashboard/{issue-service-singleton-Bnqv1uaa.js → issue-service-singleton-B8iu_TKV.js} +1 -1
  68. package/dist/dashboard/{issue-service-singleton-_AquSBTl.js → issue-service-singleton-CDGt__oQ.js} +2 -2
  69. package/dist/dashboard/{issue-service-singleton-_AquSBTl.js.map → issue-service-singleton-CDGt__oQ.js.map} +1 -1
  70. package/dist/dashboard/{lifecycle-CGKm7doE.js → lifecycle-CEg4sHZH.js} +4 -4
  71. package/dist/dashboard/{lifecycle-CGKm7doE.js.map → lifecycle-CEg4sHZH.js.map} +1 -1
  72. package/dist/dashboard/{merge-agent-Dh8Y9PDO.js → merge-agent-7nBiCytp.js} +1 -1
  73. package/dist/dashboard/{merge-agent-BcgAYStl.js → merge-agent-C81xPIGd.js} +6 -6
  74. package/dist/dashboard/{merge-agent-BcgAYStl.js.map → merge-agent-C81xPIGd.js.map} +1 -1
  75. package/dist/dashboard/merge-ops-DE_6CnmZ.js +2 -0
  76. package/dist/dashboard/{merge-train-DmxlYbEo.js → merge-train-D3qzRccL.js} +2 -2
  77. package/dist/dashboard/{merge-train-DmxlYbEo.js.map → merge-train-D3qzRccL.js.map} +1 -1
  78. package/dist/dashboard/{merge-train-deps-DBHsOWt9.js → merge-train-deps-Be5ElC_P.js} +2 -2
  79. package/dist/dashboard/{merge-train-deps-DBHsOWt9.js.map → merge-train-deps-Be5ElC_P.js.map} +1 -1
  80. package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js → orphan-proposed-reconciler-Boq_XaRe.js} +3 -3
  81. package/dist/dashboard/{orphan-proposed-reconciler-Db8tUhce.js.map → orphan-proposed-reconciler-Boq_XaRe.js.map} +1 -1
  82. package/dist/dashboard/{orphan-proposed-reconciler-mXV--tcB.js → orphan-proposed-reconciler-C0sPnDq_.js} +1 -1
  83. package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js → reconstruct-cache-C2pCltOq.js} +3 -3
  84. package/dist/dashboard/{reconstruct-cache-_tDZ2D72.js.map → reconstruct-cache-C2pCltOq.js.map} +1 -1
  85. package/dist/dashboard/{remote-completion-Bklh6loJ.js → remote-completion-BmxD5UMQ.js} +3 -3
  86. package/dist/dashboard/{remote-completion-Bklh6loJ.js.map → remote-completion-BmxD5UMQ.js.map} +1 -1
  87. package/dist/dashboard/resume-C-nxu9Y4.js +3 -0
  88. package/dist/dashboard/{review-agent-phzqNqIs.js → review-agent-D6P6d3pz.js} +26 -17
  89. package/dist/dashboard/review-agent-D6P6d3pz.js.map +1 -0
  90. package/dist/dashboard/review-agent-DTINEffB.js +3 -0
  91. package/dist/dashboard/{review-context-CCq56O_Y.js → review-context-Dyo0tsBa.js} +2 -2
  92. package/dist/dashboard/{review-context-CCq56O_Y.js.map → review-context-Dyo0tsBa.js.map} +1 -1
  93. package/dist/dashboard/{review-status-BQjqQXZG.js → review-status-B5ZASOo4.js} +18 -5
  94. package/dist/dashboard/review-status-B5ZASOo4.js.map +1 -0
  95. package/dist/dashboard/{review-status-bBkqUj5i.js → review-status-h7BXIA4z.js} +1 -1
  96. package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js → review-verdict-feedback-B70VbfRL.js} +4 -4
  97. package/dist/dashboard/{review-verdict-feedback-wqnikhMK.js.map → review-verdict-feedback-B70VbfRL.js.map} +1 -1
  98. package/dist/dashboard/{review-verdict-feedback-cZh9Wi9G.js → review-verdict-feedback-CorgH49f.js} +1 -1
  99. package/dist/dashboard/{sequence-io-B2GmilSI.js → sequence-io-6YP8fVpH.js} +2 -2
  100. package/dist/dashboard/{sequence-io-B2GmilSI.js.map → sequence-io-6YP8fVpH.js.map} +1 -1
  101. package/dist/dashboard/{sequencer-agent-D8pW2aU_.js → sequencer-agent-ja-nkEc1.js} +5 -5
  102. package/dist/dashboard/{sequencer-agent-D8pW2aU_.js.map → sequencer-agent-ja-nkEc1.js.map} +1 -1
  103. package/dist/dashboard/server.js +66 -55
  104. package/dist/dashboard/server.js.map +1 -1
  105. package/dist/dashboard/{service-CILd-uOt.js → service-HKDrIy0h.js} +24 -24
  106. package/dist/dashboard/{service-CILd-uOt.js.map → service-HKDrIy0h.js.map} +1 -1
  107. package/dist/dashboard/{smart-compaction-Bgp_gwcA.js → smart-compaction-L0LSnF0_.js} +1 -1
  108. package/dist/dashboard/{smart-compaction-CrP90OFE.js → smart-compaction-uke1npUz.js} +2 -2
  109. package/dist/dashboard/{smart-compaction-CrP90OFE.js.map → smart-compaction-uke1npUz.js.map} +1 -1
  110. package/dist/dashboard/{spawn-planning-session-D15KbPOy.js → spawn-planning-session-CSMWco8B.js} +1 -1
  111. package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js → spawn-planning-session-CXc0NkwV.js} +3 -3
  112. package/dist/dashboard/{spawn-planning-session-BmJWHSCu.js.map → spawn-planning-session-CXc0NkwV.js.map} +1 -1
  113. package/dist/dashboard/{specialist-context-CwKkcXL9.js → specialist-context-BGCJ24LH.js} +3 -3
  114. package/dist/dashboard/{specialist-context-CwKkcXL9.js.map → specialist-context-BGCJ24LH.js.map} +1 -1
  115. package/dist/dashboard/{specialist-logs-CvDxquV_.js → specialist-logs--Uz-r8ia.js} +2 -2
  116. package/dist/dashboard/{specialist-logs-CvDxquV_.js.map → specialist-logs--Uz-r8ia.js.map} +1 -1
  117. package/dist/dashboard/{specialists-BCjcG4uE.js → specialists-Ca9XSBCS.js} +1 -1
  118. package/dist/dashboard/{specialists-D07ybnam.js → specialists-DEfrAbWs.js} +36 -36
  119. package/dist/dashboard/{specialists-D07ybnam.js.map → specialists-DEfrAbWs.js.map} +1 -1
  120. package/dist/dashboard/{specialists-F2uMO4QS.js → specialists-DeZx9xom.js} +1 -1
  121. package/dist/dashboard/{specialists-BYi0k8CZ.js → specialists-nZ0mRwpZ.js} +12 -12
  122. package/dist/dashboard/{specialists-BYi0k8CZ.js.map → specialists-nZ0mRwpZ.js.map} +1 -1
  123. package/dist/dashboard/{summary-fork-DedqguZb.js → summary-fork-2a1L2DIU.js} +2 -2
  124. package/dist/dashboard/{summary-fork-EjvOkEPl.js → summary-fork-Xn0QUNmb.js} +3 -3
  125. package/dist/dashboard/{summary-fork-EjvOkEPl.js.map → summary-fork-Xn0QUNmb.js.map} +1 -1
  126. package/dist/dashboard/{teardown-workspace-5LCVbjfq.js → teardown-workspace-DhA6oAIK.js} +2 -2
  127. package/dist/dashboard/{teardown-workspace-5LCVbjfq.js.map → teardown-workspace-DhA6oAIK.js.map} +1 -1
  128. package/dist/dashboard/{test-agent-queue-qfsq6uaC.js → test-agent-queue-B_ukPyIK.js} +3 -3
  129. package/dist/dashboard/{test-agent-queue-qfsq6uaC.js.map → test-agent-queue-B_ukPyIK.js.map} +1 -1
  130. package/dist/dashboard/{uat-train-CtNGgXxj.js → uat-train-BLBAbJ8Y.js} +10 -10
  131. package/dist/dashboard/{uat-train-CtNGgXxj.js.map → uat-train-BLBAbJ8Y.js.map} +1 -1
  132. package/dist/dashboard/{uat-train-C3hWXtip.js → uat-train-DPfGR9_I.js} +1 -1
  133. package/dist/dashboard/{verification-runner-DuZRHjPU.js → verification-runner-KTGTNugM.js} +1 -1
  134. package/dist/dashboard/{verification-runner-BPAhPWR7.js → verification-runner-R2-4XgMO.js} +4 -4
  135. package/dist/dashboard/{verification-runner-BPAhPWR7.js.map → verification-runner-R2-4XgMO.js.map} +1 -1
  136. package/dist/dashboard/{workflows-H-COJ84X.js → workflows-CSKX5MhC.js} +7 -7
  137. package/dist/dashboard/{workflows-H-COJ84X.js.map → workflows-CSKX5MhC.js.map} +1 -1
  138. package/dist/dashboard/workflows-DVwXauef.js +2 -0
  139. package/dist/dashboard/{workspaces-CRU9_vRn.js → workspaces-DXLDgXo2.js} +32 -32
  140. package/dist/dashboard/{workspaces-CRU9_vRn.js.map → workspaces-DXLDgXo2.js.map} +1 -1
  141. package/dist/docs-index.sqlite +0 -0
  142. package/dist/{feedback-target-DTGAhFkM.js → feedback-target-BsKN4Tr_.js} +4 -4
  143. package/dist/{feedback-target-DTGAhFkM.js.map → feedback-target-BsKN4Tr_.js.map} +1 -1
  144. package/dist/{feedback-target-DutF088V.js → feedback-target-DM1WzOKy.js} +1 -1
  145. package/dist/{harness-resolve-Bz1prk6B.js → harness-resolve-B-YDNwNW.js} +2 -2
  146. package/dist/{harness-resolve-Bz1prk6B.js.map → harness-resolve-B-YDNwNW.js.map} +1 -1
  147. package/dist/{issue-lifecycle-fVqvCdKM.js → issue-lifecycle-Bc1l3U9f.js} +7 -7
  148. package/dist/{issue-lifecycle-fVqvCdKM.js.map → issue-lifecycle-Bc1l3U9f.js.map} +1 -1
  149. package/dist/{issue-service-singleton-CZm8p6Rj.js → issue-service-singleton-D-8Q6tdT.js} +1 -1
  150. package/dist/{issue-service-singleton-Cr73JAaR.js → issue-service-singleton-D1433I7n.js} +2 -2
  151. package/dist/{issue-service-singleton-Cr73JAaR.js.map → issue-service-singleton-D1433I7n.js.map} +1 -1
  152. package/dist/{lifecycle-CR6tZfl1.js → lifecycle-SmrqysDf.js} +7 -7
  153. package/dist/{lifecycle-CR6tZfl1.js.map → lifecycle-SmrqysDf.js.map} +1 -1
  154. package/dist/{lifecycle-D_EWiUjk.js → lifecycle-mRMyiIXs.js} +2 -2
  155. package/dist/{merge-agent-LM4avOtA.js → merge-agent-B9hIZ6Z0.js} +6 -6
  156. package/dist/{merge-agent-LM4avOtA.js.map → merge-agent-B9hIZ6Z0.js.map} +1 -1
  157. package/dist/{merge-agent-Bj046HE1.js → merge-agent-DUj22cXL.js} +1 -1
  158. package/dist/{merge-fulfillment-DrnC1Kmg.js → merge-fulfillment-__P4WE-T.js} +3 -3
  159. package/dist/{merge-fulfillment-DrnC1Kmg.js.map → merge-fulfillment-__P4WE-T.js.map} +1 -1
  160. package/dist/{reconstruct-cache-NPqoAUg-.js → reconstruct-cache-9SWp82SQ.js} +3 -3
  161. package/dist/{reconstruct-cache-NPqoAUg-.js.map → reconstruct-cache-9SWp82SQ.js.map} +1 -1
  162. package/dist/{records-BIkN1LQ4.js → records-CTmvuRzz.js} +14 -1
  163. package/dist/{records-BIkN1LQ4.js.map → records-CTmvuRzz.js.map} +1 -1
  164. package/dist/records-backfill-BQcmTmwF.js +2 -0
  165. package/dist/{records-backfill-HM0Pln6N.js → records-backfill-BrNsv_zT.js} +2 -2
  166. package/dist/{records-backfill-HM0Pln6N.js.map → records-backfill-BrNsv_zT.js.map} +1 -1
  167. package/dist/resume-Bn4kDk9x.js +3 -0
  168. package/dist/{review-agent-DFosQzCG.js → review-agent-PhqOhpx3.js} +24 -16
  169. package/dist/{review-agent-DFosQzCG.js.map → review-agent-PhqOhpx3.js.map} +1 -1
  170. package/dist/review-agent-hLbRRxZF.js +3 -0
  171. package/dist/{review-status-iJwfEoNX.js → review-status-B_XXomV0.js} +6 -6
  172. package/dist/{review-status-iJwfEoNX.js.map → review-status-B_XXomV0.js.map} +1 -1
  173. package/dist/{review-status-BbNVlXh1.js → review-status-Bo-EFVKh.js} +1 -1
  174. package/dist/{review-verdict-feedback-BGPw3z1X.js → review-verdict-feedback-CXHkoiKZ.js} +1 -1
  175. package/dist/{review-verdict-feedback-CCZ05atV.js → review-verdict-feedback-DGV13LDb.js} +4 -4
  176. package/dist/{review-verdict-feedback-CCZ05atV.js.map → review-verdict-feedback-DGV13LDb.js.map} +1 -1
  177. package/dist/{sequence-io-CcxZNHcD.js → sequence-io-Cg4NHAco.js} +1 -1
  178. package/dist/{sequence-io-Cx1usxG3.js → sequence-io-CyxvNHfA.js} +2 -2
  179. package/dist/{sequence-io-Cx1usxG3.js.map → sequence-io-CyxvNHfA.js.map} +1 -1
  180. package/dist/{smart-compaction-ByUEgQkF.js → smart-compaction-ByKXTQNy.js} +2 -2
  181. package/dist/{smart-compaction-ByUEgQkF.js.map → smart-compaction-ByKXTQNy.js.map} +1 -1
  182. package/dist/{smart-compaction-DfLe_CtH.js → smart-compaction-U6e_uB1E.js} +1 -1
  183. package/dist/{specialist-context-McuGkaeR.js → specialist-context-BUTadO-U.js} +3 -3
  184. package/dist/{specialist-context-McuGkaeR.js.map → specialist-context-BUTadO-U.js.map} +1 -1
  185. package/dist/{specialist-logs-CUUJ4eiB.js → specialist-logs-pEMCTtGZ.js} +2 -2
  186. package/dist/{specialist-logs-CUUJ4eiB.js.map → specialist-logs-pEMCTtGZ.js.map} +1 -1
  187. package/dist/{specialists-BSrylD1S.js → specialists-BGFnrQ8n.js} +1 -1
  188. package/dist/{specialists-B0elgInc.js → specialists-DeTkkp8b.js} +12 -12
  189. package/dist/{specialists-B0elgInc.js.map → specialists-DeTkkp8b.js.map} +1 -1
  190. package/dist/{summary-fork-DskMRnyb.js → summary-fork-CgzThE2-.js} +2 -2
  191. package/dist/{summary-fork-CPUTKcAT.js → summary-fork-Dx1RMuvU.js} +3 -3
  192. package/dist/{summary-fork-CPUTKcAT.js.map → summary-fork-Dx1RMuvU.js.map} +1 -1
  193. package/dist/{sync-BiNVQHBf.js → sync-Bsww9tuj.js} +2 -2
  194. package/dist/{sync-BiNVQHBf.js.map → sync-Bsww9tuj.js.map} +1 -1
  195. package/dist/sync-CylE_SjB.js +2 -0
  196. package/dist/{verdict-restore-DQiV7_fe.js → verdict-restore-DGpX0ZOJ.js} +5 -5
  197. package/dist/{verdict-restore-DQiV7_fe.js.map → verdict-restore-DGpX0ZOJ.js.map} +1 -1
  198. package/dist/{webhook-handlers-DHBSEDEm.js → webhook-handlers-BjsfIL7u.js} +3 -3
  199. package/dist/{webhook-handlers-DHBSEDEm.js.map → webhook-handlers-BjsfIL7u.js.map} +1 -1
  200. package/dist/{workspace-migrate-duUsWNpd.js → workspace-migrate-BBQ8nAnP.js} +2 -2
  201. package/dist/{workspace-migrate-duUsWNpd.js.map → workspace-migrate-BBQ8nAnP.js.map} +1 -1
  202. package/dist/{workspace-reap-DLCIoJmn.js → workspace-reap-12O9354k.js} +2 -2
  203. package/dist/{workspace-reap-DLCIoJmn.js.map → workspace-reap-12O9354k.js.map} +1 -1
  204. package/package.json +1 -1
  205. package/scripts/file-size-baseline.txt +1 -1
  206. package/dist/cli-DBWG12XK.js +0 -3
  207. package/dist/dashboard/cli-CJDWXE_Y.js +0 -3
  208. package/dist/dashboard/deacon-swarm-D1iAUX-P.js +0 -2
  209. package/dist/dashboard/inspect-agent-vm88mLYr.js +0 -2
  210. package/dist/dashboard/merge-ops-QdBiQ8XB.js +0 -2
  211. package/dist/dashboard/resume-DnIsmIgC.js +0 -3
  212. package/dist/dashboard/review-agent-DgySDb-G.js +0 -3
  213. package/dist/dashboard/review-agent-phzqNqIs.js.map +0 -1
  214. package/dist/dashboard/review-status-BQjqQXZG.js.map +0 -1
  215. package/dist/dashboard/workflows-Cz8OtiB4.js +0 -2
  216. package/dist/records-backfill-DEgtRyKL.js +0 -2
  217. package/dist/resume-DGymuXZM.js +0 -3
  218. package/dist/review-agent-DjVadURa.js +0 -3
  219. package/dist/sync-4jqukkxz.js +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"summary-fork-CPUTKcAT.js","names":[],"sources":["../src/lib/conversations/transcript-adapter.ts","../src/lib/conversations/handoff-paths.ts","../src/lib/conversations/summary-fork.ts"],"sourcesContent":["/**\n * Conversation transcript adapter.\n *\n * Each agent harness writes its session transcript in its own JSONL shape:\n *\n * - Claude Code records have top-level `type: 'user'|'assistant'|'system'`,\n * with `entry.message.content` as a block array whose blocks are\n * `text|tool_use|tool_result|thinking`.\n * - Pi records have top-level `type: 'message'|'session'|'model_change'|\n * 'thinking_level_change'|...`, with the user/assistant role nested in\n * `entry.message.role` and blocks of type `text|thinking|toolCall|toolResult`.\n * - Future harnesses (Codex, etc.) will have their own shapes.\n *\n * The handoff authoring pipeline doesn't care about any of that. It needs\n * one thing from each harness: a canonical \"<conversation>...</conversation>\"\n * text it can feed the authoring model.\n *\n * This module provides that abstraction. Adding a new harness is two short\n * functions (resolveSessionFile + serialize) plus a registry entry — the\n * fork pipeline never needs to learn about the new harness.\n */\nimport { existsSync } from 'node:fs';\nimport { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { Effect } from 'effect';\n\nimport type { LegacyConversation as Conversation } from '../overdeck/conversations.js';\nimport type { RuntimeName } from '../runtimes/types.js';\nimport { sessionFilePath } from '../paths.js';\nimport {\n parseEntries as parseClaudeCodeEntries,\n serializeConversation as serializeClaudeCodeConversation,\n generateSmartSummary,\n summarizeSerializedText,\n} from './smart-compaction.js';\n\nexport interface CompactSummaryOptions {\n /** Model for the summarizer LLM. */\n model?: string;\n /** Use the rich (Claude-native-style) summary prompt variant. */\n richMode?: boolean;\n /** Include thinking blocks in the serialized source. Default: true. */\n includeThinking?: boolean;\n /** Harness backend the summarizer LLM runs on. Default: 'claude-code'. */\n harness?: RuntimeName;\n /** Per-LLM-call timeout (ms). */\n timeoutMs?: number;\n}\n\nexport interface ConversationTranscriptAdapter {\n /** Display name for logging/errors. */\n readonly name: RuntimeName;\n\n /** Plain forks copy raw Claude JSONL and spawn Claude with `--resume` —\n * only Claude Code can be the source of a plain fork. */\n readonly supportsPlainForkAsSource: boolean;\n\n /** Source-authored handoff requires delivering a prompt to the live source\n * agent and waiting for it to write a sentinel file. Claude Code supports\n * this via deliverAgentMessage; Pi has no hook-equivalent signaling channel\n * for the .done sentinel (see PAN-1134), so source authoring is gated off\n * for Pi until that lands. */\n readonly supportsSourceAuthoredHandoff: boolean;\n\n /** Resolve the path to the source conversation's transcript file.\n * Returns null if the conversation has no transcript yet (e.g. session\n * just started and hasn't produced a JSONL line). */\n resolveSessionFile(conv: Conversation): Promise<string | null>;\n\n /** Serialize the transcript into a harness-agnostic canonical text that\n * the handoff prompt template can embed verbatim. Each turn is rendered\n * with a `[user]` / `[assistant]` header followed by text and tool-use\n * summaries; thinking blocks are optional (defaults to including them).\n */\n serializeTranscript(sessionFile: string, options?: { includeThinking?: boolean }): Promise<string>;\n\n /**\n * Produce a model-generated compact summary of the transcript, suitable for\n * seeding a summary fork or feeding the handoff authoring prompt.\n *\n * claude-code uses the entry-aware smart-compaction flow (rich, file-op\n * aware); other harnesses serialize the transcript and run a generic\n * chunk-and-merge summarizer over the text. The `harness` option selects the\n * summarizer LLM backend and is independent of which adapter (= source\n * harness) produced the transcript.\n */\n compactSummary(\n sessionFile: string,\n options?: CompactSummaryOptions,\n ): Promise<{ summary: string; summaryModel: string | null }>;\n}\n\n// ─── Claude Code ──────────────────────────────────────────────────────────\n\nconst claudeCodeAdapter: ConversationTranscriptAdapter = {\n name: 'claude-code',\n supportsPlainForkAsSource: true,\n supportsSourceAuthoredHandoff: true,\n\n async resolveSessionFile(conv) {\n if (!conv.claudeSessionId) return null;\n const path = sessionFilePath(conv.cwd, conv.claudeSessionId);\n return existsSync(path) ? path : null;\n },\n\n async serializeTranscript(sessionFile, options) {\n const entries = await parseClaudeCodeEntries(sessionFile);\n return serializeClaudeCodeConversation(entries, options?.includeThinking ?? true);\n },\n\n async compactSummary(sessionFile, options) {\n // Claude Code keeps the entry-aware smart-compaction flow: it parses the\n // JSONL into typed entries, finds compact boundaries, and carries file-op\n // detail into the summary. This is richer than text-only chunking.\n const result = await Effect.runPromise(\n generateSmartSummary({\n jsonlPath: sessionFile,\n model: options?.model,\n richMode: options?.richMode ?? false,\n mode: 'fork',\n includeThinkingInSummary: options?.includeThinking ?? true,\n harness: options?.harness ?? 'claude-code',\n }),\n );\n return { summary: result.summary, summaryModel: result.summaryModel };\n },\n};\n\n// ─── Pi ───────────────────────────────────────────────────────────────────\n\n/**\n * Pi sessions live at:\n * `~/.overdeck/agents/<tmuxSession>/sessions/<iso-timestamp>_<id>.jsonl`\n *\n * Pi may rotate session files (e.g. on resume), so we pick the\n * newest by filename — filenames sort lexicographically by their\n * ISO timestamp prefix so this is deterministic.\n */\nasync function resolvePiSessionFileFromTmux(tmuxSession: string): Promise<string | null> {\n const sessionDir = join(homedir(), '.overdeck', 'agents', tmuxSession, 'sessions');\n if (!existsSync(sessionDir)) return null;\n try {\n const entries = (await readdir(sessionDir)).filter((name) => name.endsWith('.jsonl'));\n if (entries.length === 0) return null;\n entries.sort();\n return join(sessionDir, entries[entries.length - 1]!);\n } catch {\n return null;\n }\n}\n\ninterface PiEntry {\n type?: string;\n message?: {\n role?: string;\n content?: Array<{\n type?: string;\n text?: string;\n thinking?: string;\n name?: string;\n arguments?: unknown;\n }>;\n };\n}\n\nfunction serializePiEntry(entry: PiEntry, includeThinking: boolean): string | undefined {\n if (entry.type !== 'message') return undefined;\n const role = entry.message?.role;\n if (role !== 'user' && role !== 'assistant') return undefined;\n\n const content = entry.message?.content;\n if (!Array.isArray(content)) return undefined;\n\n const lines: string[] = [`[${role}]`];\n for (const block of content) {\n if (block?.type === 'text' && typeof block.text === 'string' && block.text.trim()) {\n lines.push(block.text);\n } else if (block?.type === 'thinking' && includeThinking && typeof block.thinking === 'string' && block.thinking.trim()) {\n lines.push(`[thinking]\\n${block.thinking}`);\n } else if (block?.type === 'toolCall' && typeof block.name === 'string') {\n let args = '';\n try {\n args = JSON.stringify(block.arguments).slice(0, 500);\n } catch {\n args = '<unserializable>';\n }\n lines.push(`[tool_use: ${block.name}]\\n${args}`);\n }\n // toolResult, function-call envelopes, etc. are intentionally skipped — the\n // surface we care about for handoff is what the user said, what the agent\n // said, what tools it ran, and what it was thinking. Tool outputs balloon\n // the transcript without adding signal a handoff doc needs.\n }\n if (lines.length === 1) return undefined; // header only — no content\n return lines.join('\\n');\n}\n\nconst piAdapter: ConversationTranscriptAdapter = {\n name: 'ohmypi',\n supportsPlainForkAsSource: false,\n supportsSourceAuthoredHandoff: false,\n\n async resolveSessionFile(conv) {\n return resolvePiSessionFileFromTmux(conv.tmuxSession);\n },\n\n async serializeTranscript(sessionFile, options) {\n const includeThinking = options?.includeThinking ?? true;\n const content = await readFile(sessionFile, 'utf-8');\n const parts: string[] = [];\n for (const line of content.split('\\n')) {\n if (!line.trim()) continue;\n let entry: PiEntry;\n try {\n entry = JSON.parse(line) as PiEntry;\n } catch {\n continue;\n }\n const serialized = serializePiEntry(entry, includeThinking);\n if (serialized) parts.push(serialized);\n }\n return parts.join('\\n\\n');\n },\n\n async compactSummary(sessionFile, options) {\n // Pi's JSONL shape is not what the Claude-Code entry parser understands, so\n // we serialize the transcript through this adapter first, then run the\n // generic text chunk-and-merge summarizer over it.\n const serialized = await piAdapter.serializeTranscript(sessionFile, {\n includeThinking: options?.includeThinking ?? true,\n });\n if (!serialized.trim()) {\n return { summary: '', summaryModel: null };\n }\n const summary = await summarizeSerializedText(serialized, {\n model: options?.model,\n richMode: options?.richMode ?? false,\n harness: options?.harness ?? 'claude-code',\n timeoutMs: options?.timeoutMs,\n });\n return { summary, summaryModel: options?.model ?? null };\n },\n};\n\n// ─── Registry ─────────────────────────────────────────────────────────────\n\nconst REGISTRY: Record<RuntimeName, ConversationTranscriptAdapter> = {\n 'claude-code': claudeCodeAdapter,\n 'ohmypi': piAdapter,\n 'codex': claudeCodeAdapter,\n};\n\n/**\n * Look up the transcript adapter for a harness. Unknown harnesses default to\n * the Claude Code adapter — that matches the conservative behavior of the rest\n * of the codebase (see getHarness() in @overdeck/contracts).\n */\nexport function getTranscriptAdapter(harness: RuntimeName | undefined): ConversationTranscriptAdapter {\n return REGISTRY[harness ?? 'claude-code'] ?? claudeCodeAdapter;\n}\n","import { mkdir } from 'fs/promises';\nimport { join } from 'path';\nimport { getHandoffsDir } from '../paths.js';\n\nconst SAFE_PATH_SEGMENT = /^[A-Za-z0-9._-]+$/;\n\nexport interface HandoffPaths {\n docPath: string;\n sentinelPath: string;\n}\n\nexport function createHandoffPaths(sourceConvId: string, isoTimestamp: string): HandoffPaths {\n const convId = sourceConvId.trim();\n const timestamp = isoTimestamp.trim();\n\n if (!SAFE_PATH_SEGMENT.test(convId)) {\n throw new Error(`Invalid handoff conversation id: ${sourceConvId}`);\n }\n if (timestamp.length === 0 || timestamp.includes('/') || timestamp.includes('\\\\')) {\n throw new Error(`Invalid handoff timestamp: ${isoTimestamp}`);\n }\n\n const docPath = join(getHandoffsDir(), `${convId}-${timestamp}.md`);\n return {\n docPath,\n sentinelPath: `${docPath}.done`,\n };\n}\n\nexport async function ensureHandoffsDir(): Promise<string> {\n const handoffsDir = getHandoffsDir();\n await mkdir(handoffsDir, { recursive: true });\n return handoffsDir;\n}\n","/**\n * Conversation fork pipeline.\n *\n * This module handles creating a new conversation from an existing one.\n * Two modes are supported:\n *\n * 1. Summary fork (default): The conversation history is serialized and sent\n * to an LLM summarizer (see smart-compaction.ts). The generated structured\n * summary is injected as the first user message in the new session.\n *\n * 2. Plain fork: Raw JSONL history is copied from the last compact_boundary\n * into a new session file. Thinking blocks are sanitized (converted to text)\n * to prevent signature validation errors on cross-model resumes.\n *\n * Entry point: createSummaryFork()\n * - Reserves a new session ID and file path\n * - Generates summary (LLM, heuristic fallback, or skips for plain fork)\n * - Creates a DB record for the new conversation\n * - Returns the new conversation + session metadata\n *\n * Dashboard API: runForkPipeline() in src/dashboard/server/routes/conversations.ts\n * wires the options through and handles tmux spawn + summary injection.\n */\nimport { randomUUID } from 'node:crypto';\nimport { access, mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { Effect } from 'effect';\n\nimport type { LegacyConversation as Conversation } from '../overdeck/conversations.js';\nimport {\n createConversation,\n getConversationByName,\n recordConversationHandoff,\n updateConversationForkFallbackReason,\n} from '../overdeck/conversations.js';\nimport { encodeClaudeProjectDir, packageRoot, sessionFilePath } from '../paths.js';\nimport { loadConfigSync } from '../config-yaml.js';\nimport { deliverAgentMessage } from '../agents.js';\nimport { runModelSummary } from './smart-compaction.js';\nimport { getTranscriptAdapter } from './transcript-adapter.js';\nimport { createHandoffPaths, ensureHandoffsDir, type HandoffPaths } from './handoff-paths.js';\nimport type { RuntimeName } from '../runtimes/types.js';\nimport { FsError } from '../errors.js';\nimport { getWorkspaceStackHealth } from '../workspace/stack-health.js';\n\nexport type SummaryForkMode = 'summary' | 'plain' | 'handoff';\nexport type HandoffAuthor = 'source' | 'external';\n\nexport interface SummaryForkOptions {\n model?: string;\n cwd?: string;\n harness?: RuntimeName;\n localSummaryOnly?: boolean;\n forkMode?: SummaryForkMode;\n focus?: string;\n handoffTimeoutMs?: number;\n handoffPollIntervalMs?: number;\n /** When true, include thinking block content in the serialized conversation sent to the summary model. Default: true. */\n includeThinkingInSummary?: boolean;\n /**\n * Who authors the handoff document when forkMode === 'handoff'.\n *\n * - 'source' (legacy): deliver the handoff prompt to the live source agent\n * via deliverAgentMessage and wait for it to write the doc + .done sentinel.\n * Requires the source conversation to be alive. Pollutes the source's\n * context. Uses whatever model/harness the source is running on.\n * - 'external' (default): spawn a separate authoring session with the chosen\n * model/harness that reads the source JSONL transcript and emits the doc.\n * Does not touch the source conversation. Works on ended sources.\n */\n handoffAuthor?: HandoffAuthor;\n /** Model for the external handoff authoring session. Ignored when handoffAuthor === 'source'. */\n handoffAuthorModel?: string;\n /** Harness for the external handoff authoring session. Ignored when handoffAuthor === 'source'. */\n handoffAuthorHarness?: RuntimeName;\n}\n\nexport interface SummaryForkResult {\n conversation: Conversation;\n sessionId: string;\n sessionFile: string;\n summary: string;\n summaryModel: string | null;\n forkMode: SummaryForkMode;\n handoffDocPath: string | null;\n forkFallbackReason: string | null;\n}\n\nconst FORK_WAIT_INSTRUCTION = `\\n---\\n\\n**Do not take any action.** This is context from a prior conversation fork. Acknowledge the summary and wait for the user's next instruction.`;\nconst DEFAULT_HANDOFF_TIMEOUT_MS = 300_000;\nconst DEFAULT_HANDOFF_POLL_INTERVAL_MS = 1_000;\nconst NO_HANDOFF_FOCUS = 'No specific focus was provided.';\n\nexport type HandoffDocValidation =\n | { ok: true }\n | { ok: false; reason: string };\n\nexport interface RequestHandoffOptions {\n timeoutMs?: number;\n pollIntervalMs?: number;\n now?: Date;\n}\n\nexport interface RequestHandoffResult {\n docPath: string;\n docText: string;\n}\n\nexport class HandoffStallError extends Error {\n constructor(\n public readonly docPath: string,\n public readonly sentinelPath: string,\n public readonly timeoutMs: number,\n ) {\n super(`Timed out waiting ${timeoutMs}ms for handoff document and sentinel: ${docPath}, ${sentinelPath}`);\n this.name = 'HandoffStallError';\n }\n}\n\nexport class HandoffValidationError extends Error {\n constructor(\n public readonly docPath: string,\n public readonly reason: string,\n ) {\n super(`Invalid handoff document ${docPath}: ${reason}`);\n this.name = 'HandoffValidationError';\n }\n}\n\n/**\n * Strip a wrapping ``` fenced code block from the doc body if the LLM\n * helpfully wrapped its Markdown output in a fence. Returns the inner\n * content if a fence was detected, otherwise returns the trimmed input.\n */\nexport function sanitizeHandoffDoc(text: string): string {\n const trimmed = text.trim();\n const fenceMatch = trimmed.match(/^```(?:markdown|md)?\\s*\\n([\\s\\S]*?)\\n```\\s*$/);\n return fenceMatch?.[1]?.trim() ?? trimmed;\n}\n\nexport function validateHandoffDoc(text: string): HandoffDocValidation {\n const sanitized = sanitizeHandoffDoc(text);\n if (sanitized.length < 200) {\n return { ok: false, reason: 'handoff document must be at least 200 characters' };\n }\n // Accept any heading depth (H1-H6), case-insensitive, with an optional\n // trailing colon. Real-world LLM outputs vary on heading conventions; the\n // failure mode of a too-strict validator is silent fallback to summary fork\n // with no surface to the user, which is the worst outcome.\n if (!/^#{1,6}\\s+suggested skills\\s*:?\\s*$/imu.test(sanitized)) {\n return { ok: false, reason: 'handoff document must contain a Suggested skills heading' };\n }\n return { ok: true };\n}\n\nfunction renderHandoffPrompt(template: string, focus: string | undefined, outputPath: string): string {\n const safeFocus = focus?.trim() || NO_HANDOFF_FOCUS;\n return template\n .split('{{focus}}').join(safeFocus)\n .split('{{outputPath}}').join(outputPath);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function waitForHandoffDoc(paths: HandoffPaths, timeoutMs: number, pollIntervalMs: number): Promise<string> {\n const deadline = Date.now() + timeoutMs;\n\n while (true) {\n try {\n await access(paths.docPath);\n await access(paths.sentinelPath);\n return await readFile(paths.docPath, 'utf-8');\n } catch {\n const remainingMs = deadline - Date.now();\n if (remainingMs <= 0) {\n throw new HandoffStallError(paths.docPath, paths.sentinelPath, timeoutMs);\n }\n await delay(Math.min(pollIntervalMs, remainingMs));\n }\n }\n}\n\nconst DEFAULT_HANDOFF_AUTHOR_MODEL = 'claude-sonnet-4-6';\nconst HANDOFF_AUTHOR_TIMEOUT_MS = 300_000;\n// When the raw transcript exceeds this many characters, pre-compact it via\n// generateSmartSummary (which chunks internally) before sending to the\n// handoff authoring model. Threshold chosen to keep the final prompt\n// comfortably under Haiku's 200k-token window (~800k chars at 4 chars/token,\n// minus headroom for the template, focus, and a response budget). The exact\n// value isn't critical — anything that triggers compaction for \"long\"\n// transcripts is fine.\nconst HANDOFF_TRANSCRIPT_COMPACT_THRESHOLD = 100_000;\n\nfunction renderExternalHandoffPrompt(template: string, focus: string | undefined, transcript: string, outputPath: string): string {\n const safeFocus = focus?.trim() || NO_HANDOFF_FOCUS;\n return template\n .split('{{focus}}').join(safeFocus)\n .split('{{outputPath}}').join(outputPath)\n .split('{{transcript}}').join(transcript);\n}\n\n/**\n * Author a handoff document from an external authoring session.\n *\n * Reads the source JSONL transcript and asks a fresh, isolated model session\n * (with the chosen model + harness) to write the handoff Markdown. The source\n * conversation is never contacted — its context is not polluted.\n *\n * Returns the same { docPath, docText } shape as requestHandoffFromAgent so\n * callers can substitute one for the other.\n */\nexport async function authorHandoffExternal(\n sourceConv: Conversation,\n sourceSessionFile: string,\n focus: string | undefined,\n model: string | undefined,\n harness: RuntimeName | undefined,\n options: { now?: Date } = {},\n): Promise<RequestHandoffResult> {\n await ensureHandoffsDir();\n const timestamp = (options.now ?? new Date()).toISOString();\n const paths = createHandoffPaths(sourceConv.name, timestamp);\n\n const effectiveModel = model ?? DEFAULT_HANDOFF_AUTHOR_MODEL;\n const effectiveHarness: RuntimeName = harness ?? 'claude-code';\n\n // The authoring harness decides the prompt template: Claude Code's `Write`\n // tool vs Pi's lowercase `write` tool, with harness-specific phrasing about\n // how the model should call it. Both templates share the same H2 contract,\n // and the file-on-disk read below is harness-independent.\n const templateName = effectiveHarness === 'ohmypi' ? 'handoff-external-pi.md' : 'handoff-external.md';\n const template = await readFile(join(packageRoot, 'roles', templateName), 'utf-8');\n\n // The source's harness decides how the transcript is read/serialized; the\n // authoring harness (model + harness picked by the user) is independent.\n const sourceAdapter = getTranscriptAdapter(sourceConv.harness ?? undefined);\n // Skip thinking blocks — they're large and the structured output we want\n // doesn't need internal reasoning, only the user/assistant exchange.\n const transcript = await sourceAdapter.serializeTranscript(sourceSessionFile, { includeThinking: false });\n\n // If the raw transcript is small enough, feed it to the model verbatim —\n // that's the richest input. For long transcripts we'd overflow any model's\n // context window in a single-pass prompt, so first compact via the chunked\n // generateSmartSummary path (which already handles chunking + merging) and\n // feed the compact summary to the handoff authoring step. The handoff\n // template treats {{transcript}} as opaque context — a compact summary\n // works as well as the raw transcript, just with less fine detail.\n let promptInput: string;\n let inputLabel: 'raw-transcript' | 'compact-summary';\n // Precompaction goes through the source adapter's compactSummary, which is\n // harness-agnostic: claude-code uses the entry-aware smart-compaction flow,\n // Pi (and future harnesses) serialize their transcript and run the generic\n // chunk-and-merge summarizer. Either way a long source transcript is\n // compacted before it reaches the authoring model's context window.\n if (transcript.length > HANDOFF_TRANSCRIPT_COMPACT_THRESHOLD) {\n console.log(`[claude-invoke] purpose=handoff-author-external | model=${effectiveModel} | harness=${effectiveHarness} | source=${sourceConv.name} | transcriptChars=${transcript.length} | precompacting=true`);\n const { config } = loadConfigSync();\n const richMode = config.conversations.richCompaction;\n const compact = await sourceAdapter.compactSummary(sourceSessionFile, {\n model: effectiveModel,\n richMode,\n includeThinking: false,\n harness: effectiveHarness,\n });\n promptInput = compact.summary;\n inputLabel = 'compact-summary';\n console.log(`[claude-invoke] purpose=handoff-author-external precompact-result | model=${effectiveModel} | compactChars=${compact.summary.length}`);\n } else {\n promptInput = transcript;\n inputLabel = 'raw-transcript';\n }\n\n const prompt = renderExternalHandoffPrompt(template, focus, promptInput, paths.docPath);\n console.log(`[claude-invoke] purpose=handoff-author-external | model=${effectiveModel} | harness=${effectiveHarness} | source=${sourceConv.name} | inputType=${inputLabel} | promptChars=${prompt.length} | outputPath=${paths.docPath}`);\n\n // The prompt tells the model to use its Write tool to author the document\n // directly at paths.docPath. We capture stdout only as a diagnostic log —\n // the source of truth for the doc is the file on disk. Writing via tool\n // avoids stdout preamble leaks like \"Here is the handoff document:\" that\n // contaminated earlier attempts.\n //\n // The authoring session is headless (`claude -p`), so the Write tool must be\n // explicitly allowlisted — otherwise `--permission-mode auto` raises a\n // permission prompt the session can never answer, the model emits \"I need\n // permission to write…\" to stdout, no file is written, and the whole fork\n // falls back to a plain summary with reason `handoff-validation` (PAN-1582).\n // Pi runs in rpc mode and auto-executes tools, so the allowlist is a no-op there.\n const stdout = await Effect.runPromise(runModelSummary(prompt, effectiveModel, HANDOFF_AUTHOR_TIMEOUT_MS, effectiveHarness, ['Write']));\n console.log(`[claude-invoke] purpose=handoff-author-external acknowledgement | model=${effectiveModel} | stdoutChars=${stdout.length} | stdoutPreview=${JSON.stringify(stdout.slice(0, 120))}`);\n\n let docText: string;\n try {\n docText = await readFile(paths.docPath, 'utf-8');\n } catch (err) {\n // The model didn't use Write — it almost certainly produced the doc on\n // stdout instead. Persist whatever we got for diagnosis, then surface\n // the failure as a validation error so the pipeline falls back to a\n // summary fork rather than spinning forever.\n const rejectedPath = `${paths.docPath}.rejected.md`;\n await writeFile(rejectedPath, stdout, 'utf-8').catch((wErr) => {\n console.warn(`[handoff-author-external] failed to persist stdout to ${rejectedPath}: ${wErr?.message ?? wErr}`);\n });\n console.warn(`[handoff-author-external] model did not call Write — stdout (${stdout.length} chars) saved to ${rejectedPath}: ${(err as { message?: string })?.message ?? err}`);\n throw new HandoffValidationError(paths.docPath, 'authoring session did not call its Write tool to create the handoff doc');\n }\n\n const validation = validateHandoffDoc(docText);\n if (!validation.ok) {\n // The file exists but its content failed the contract check. Persist\n // the rejected file for diagnosis and surface the reason.\n const rejectedPath = `${paths.docPath}.rejected.md`;\n await writeFile(rejectedPath, docText, 'utf-8').catch((err) => {\n console.warn(`[handoff-author-external] failed to persist rejected doc to ${rejectedPath}: ${err?.message ?? err}`);\n });\n console.warn(`[handoff-author-external] validation rejected file content (${validation.reason}); copy saved to ${rejectedPath}`);\n throw new HandoffValidationError(paths.docPath, validation.reason);\n }\n\n // Sanitize as a safety net in case the model wrapped the file in fences\n // despite the prompt telling it not to. Overwrite the file with the\n // cleaned text so downstream consumers see the same thing the validator\n // approved.\n const sanitized = sanitizeHandoffDoc(docText);\n if (sanitized !== docText) {\n await writeFile(paths.docPath, sanitized, 'utf-8');\n }\n await writeFile(paths.sentinelPath, '', 'utf-8');\n\n return { docPath: paths.docPath, docText: sanitized };\n}\n\nexport async function requestHandoffFromAgent(\n sourceConv: Conversation,\n focus?: string,\n options: RequestHandoffOptions = {},\n): Promise<RequestHandoffResult> {\n await ensureHandoffsDir();\n const timestamp = (options.now ?? new Date()).toISOString();\n const paths = createHandoffPaths(sourceConv.name, timestamp);\n const template = await readFile(join(packageRoot, 'roles', 'handoff.md'), 'utf-8');\n const prompt = renderHandoffPrompt(template, focus, paths.docPath);\n\n await deliverAgentMessage(sourceConv.tmuxSession, prompt, 'handoff-request');\n\n const docText = await waitForHandoffDoc(\n paths,\n options.timeoutMs ?? DEFAULT_HANDOFF_TIMEOUT_MS,\n options.pollIntervalMs ?? DEFAULT_HANDOFF_POLL_INTERVAL_MS,\n );\n const validation = validateHandoffDoc(docText);\n if (!validation.ok) {\n // The source agent already wrote the file to docPath. Move it aside to\n // .rejected.md so the next handoff attempt doesn't reuse a stale invalid\n // doc and so the operator can inspect what the agent wrote.\n const rejectedPath = `${paths.docPath}.rejected.md`;\n await writeFile(rejectedPath, docText, 'utf-8').catch((err) => {\n console.warn(`[handoff-source] failed to persist rejected output to ${rejectedPath}: ${err?.message ?? err}`);\n });\n console.warn(`[handoff-source] validation rejected source-authored doc (${validation.reason}); raw output saved to ${rejectedPath}`);\n throw new HandoffValidationError(paths.docPath, validation.reason);\n }\n\n return { docPath: paths.docPath, docText: sanitizeHandoffDoc(docText) };\n}\n\nfunction workspaceSourceFromCwd(sourceConv: Conversation): { issueId: string; workspacePath: string } | null {\n const normalizedCwd = sourceConv.cwd.replace(/\\\\/g, '/');\n const match = normalizedCwd.match(/^((?:(?:.*)\\/)?workspaces\\/feature-([a-z]+-\\d+))(?:\\/.*)?$/i);\n if (!match?.[1] || !match[2]) return null;\n return {\n issueId: sourceConv.issueId ?? match[2].toUpperCase(),\n workspacePath: match[1],\n };\n}\n\nexport async function handoffPreconditionFallbackReason(sourceConv: Conversation): Promise<string | null> {\n if (sourceConv.status === 'ended') return 'source-ended';\n\n const workspaceSource = workspaceSourceFromCwd(sourceConv);\n if (!workspaceSource) return null;\n\n try {\n await access(join(workspaceSource.workspacePath, '.devcontainer'));\n } catch {\n return null;\n }\n\n const health = await Effect.runPromise(getWorkspaceStackHealth(workspaceSource.issueId, {\n workspacePath: workspaceSource.workspacePath,\n }));\n return health.healthy ? 'source-workspace-devcontainer' : null;\n}\n\nexport function handoffFailureReason(error: unknown): string {\n if (error instanceof HandoffStallError) return 'handoff-timeout';\n if (error instanceof HandoffValidationError) return 'handoff-validation';\n return 'handoff-request-failed';\n}\n\nexport function logHandoffFallback(sourceConv: Conversation, reason: string): void {\n console.warn(`[summary-fork] handoff-fallback source=${sourceConv.name} reason=${reason}`);\n}\n\n/**\n * When a handoff falls back to a summary fork, the user's focus text would\n * otherwise be silently dropped. Prepend a small notice to the summary so the\n * successor conversation still sees what was asked, and so the user gets a\n * visible breadcrumb that the intended handoff failed.\n */\nexport function prependFallbackFocus(summary: string, focus: string | undefined, fallbackReason: string): string {\n const trimmedFocus = focus?.trim();\n if (!trimmedFocus) return summary;\n const header = [\n `**Note from Overdeck:** the intended handoff fell back to a summary fork (\\`${fallbackReason}\\`). The focus you requested is preserved below; the summary that follows is auto-generated, not an authored handoff document.`,\n '',\n '**Requested focus:**',\n '',\n `> ${trimmedFocus.split('\\n').join('\\n> ')}`,\n '',\n '---',\n '',\n ].join('\\n');\n return header + summary;\n}\n\nasync function generateSummarySeed(\n sourceSessionFile: string,\n summaryModel: string | undefined,\n localSummaryOnly: boolean | undefined,\n includeThinkingInSummary: boolean | undefined,\n summaryHarness?: RuntimeName,\n sourceHarness?: RuntimeName,\n): Promise<{ summary: string; summaryModel: string | null }> {\n if (localSummaryOnly) {\n return {\n summary: await Effect.runPromise(generateFallbackSummary(sourceSessionFile)),\n summaryModel: null,\n };\n }\n return generateSummaryForFork(sourceSessionFile, summaryModel, includeThinkingInSummary, summaryHarness, sourceHarness);\n}\n\nasync function generateFallbackSummaryPromise(jsonlPath: string): Promise<string> {\n const { readFile } = await import('node:fs/promises');\n const lines = (await readFile(jsonlPath, 'utf-8'))\n .split('\\n')\n .filter((l) => l.trim());\n\n const userMessages: string[] = [];\n const filesModified = new Set<string>();\n const toolsUsed = new Set<string>();\n\n for (const line of lines) {\n let entry: any;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n\n if (entry.type === 'user' && entry.message) {\n const content = entry.message.content;\n if (typeof content === 'string' && content.trim()) {\n if (!content.trim().startsWith('<local-command') && !content.trim().startsWith('<command-name')) {\n userMessages.push(content.trim());\n }\n } else if (Array.isArray(content)) {\n for (const block of content) {\n if (block.type === 'text' && block.text?.trim() && !block.text.trim().startsWith('<')) {\n userMessages.push(block.text.trim());\n }\n }\n }\n }\n\n if (entry.type === 'assistant' && entry.message?.content) {\n const content = entry.message.content;\n if (Array.isArray(content)) {\n for (const block of content) {\n if (block.type === 'tool_use') {\n toolsUsed.add(block.name);\n if (block.name === 'Edit' || block.name === 'Write') {\n const fp = block.input?.file_path || block.input?.path;\n if (fp) filesModified.add(fp);\n }\n }\n }\n }\n }\n }\n\n let summary = `## Conversation Summary Fork\\n\\n`;\n summary += `This is a continuation of a previous conversation, seeded with a summary of the earlier work.\\n\\n`;\n\n if (userMessages.length > 0) {\n summary += `### User Messages:\\n`;\n for (const msg of userMessages.slice(0, 10)) {\n summary += `- ${msg.slice(0, 200)}${msg.length > 200 ? '...' : ''}\\n`;\n }\n summary += '\\n';\n }\n\n if (filesModified.size > 0) {\n summary += `### Files Modified:\\n`;\n for (const f of [...filesModified].sort()) {\n summary += `- \\`${f.replace(/.*\\/overdeck\\//, '')}\\`\\n`;\n }\n summary += '\\n';\n }\n\n if (toolsUsed.size > 0) {\n summary += `### Tools Used: ${[...toolsUsed].sort().join(', ')}\\n\\n`;\n }\n\n summary += FORK_WAIT_INSTRUCTION;\n\n return summary;\n}\n\nexport async function generateSummaryForFork(\n jsonlPath: string,\n summaryModel?: string,\n includeThinkingInSummary?: boolean,\n summaryHarness: RuntimeName = 'claude-code',\n sourceHarness?: RuntimeName,\n): Promise<{ summary: string; summaryModel: string | null }> {\n if (!summaryModel) {\n // Fork summaries serialize the entire conversation in one shot. Sonnet 4.6's\n // 1M-token context handles large sessions that would overflow Haiku's 200k.\n summaryModel = 'claude-sonnet-4-6';\n }\n\n console.log(`[claude-invoke] purpose=summary-fork | model=${summaryModel} | summaryHarness=${summaryHarness} | sourceHarness=${sourceHarness ?? 'claude-code'} | source=summary-fork.ts:generateSummaryForFork | jsonl=${jsonlPath}`);\n\n const { config } = loadConfigSync();\n const richMode = config.conversations.richCompaction;\n\n // The source harness selects which transcript adapter parses/serializes the\n // JSONL (Pi and Claude Code have different shapes); the summary harness\n // selects which CLI backend runs the summarizer LLM. They are independent —\n // e.g. a Pi source can be summarized by a Claude model.\n const adapter = getTranscriptAdapter(sourceHarness);\n\n try {\n const result = await adapter.compactSummary(jsonlPath, {\n model: summaryModel,\n richMode,\n includeThinking: includeThinkingInSummary,\n harness: summaryHarness,\n });\n console.log(`[claude-invoke] SUCCESS purpose=summary-fork | model=${summaryModel} | outputChars=${result.summary.length}`);\n return { summary: result.summary + FORK_WAIT_INSTRUCTION, summaryModel };\n } catch (err: any) {\n console.error(`[claude-invoke] FAILED purpose=summary-fork | model=${summaryModel} | error=\"${err.message}\"`);\n throw err;\n }\n}async function reserveSummaryForkSessionPromise(\n cwd: string,\n): Promise<{ sessionId: string; sessionFile: string }> {\n // PAN-1862 (NFR-3): delegates to the shared session-fork primitive so the\n // conversation panel and the review convoy fork through one code path.\n const { reserveForkSession } = await import('./session-fork.js');\n return reserveForkSession(cwd);\n}\n\nasync function copySessionFromCompactBoundaryPromise(\n sourcePath: string,\n destPath: string,\n): Promise<void> {\n const { copySessionForFork } = await import('./session-fork.js');\n return copySessionForFork(sourcePath, destPath, { fullHistory: false });\n}\n\nasync function createSummaryForkPromise(\n conv: Conversation,\n options: SummaryForkOptions = {},\n): Promise<SummaryForkResult> {\n const sourceSessionFile = conv.claudeSessionId\n ? sessionFilePath(conv.cwd, conv.claudeSessionId)\n : null;\n if (!sourceSessionFile) {\n throw new Error(`No session file found for conversation ${conv.name}`);\n }\n\n const cwd = options.cwd || conv.cwd || process.cwd();\n const launchModel = options.model || conv.model;\n const summaryModel = options.model || conv.model;\n const forkMode = options.forkMode ?? 'summary';\n console.log(`[summary-fork] Forking conv=${conv.name} launchModel=${launchModel || 'default'} summaryModel=${summaryModel || 'default'} localOnly=${options.localSummaryOnly || false} forkMode=${forkMode}`);\n\n const { sessionId, sessionFile } = await Effect.runPromise(reserveSummaryForkSession(cwd));\n\n let summary: string;\n let usedSummaryModel: string | null;\n let effectiveForkMode = forkMode;\n let handoffDocPath: string | null = null;\n let forkFallbackReason: string | null = null;\n\n if (forkMode === 'plain') {\n // Plain fork: copy raw JSONL from last compact boundary (or full history)\n // into the new session file so Claude Code can --resume it directly.\n await Effect.runPromise(copySessionFromCompactBoundary(sourceSessionFile, sessionFile));\n summary = '';\n usedSummaryModel = null;\n } else if (forkMode === 'handoff') {\n const handoffAuthor: HandoffAuthor = options.handoffAuthor ?? 'external';\n if (handoffAuthor === 'external') {\n // External authoring: read the source JSONL and have a fresh authoring\n // session write the handoff doc. Source conversation is never touched.\n // Works on ended source conversations and lets the user pick the\n // authoring model/harness independently of the source.\n try {\n const handoff = await authorHandoffExternal(\n conv,\n sourceSessionFile,\n options.focus,\n options.handoffAuthorModel,\n options.handoffAuthorHarness,\n );\n summary = handoff.docText;\n usedSummaryModel = options.handoffAuthorModel ?? DEFAULT_HANDOFF_AUTHOR_MODEL;\n handoffDocPath = handoff.docPath;\n } catch (error) {\n forkFallbackReason = handoffFailureReason(error);\n effectiveForkMode = 'summary';\n logHandoffFallback(conv, forkFallbackReason);\n const result = await generateSummarySeed(sourceSessionFile, summaryModel ?? undefined, options.localSummaryOnly, options.includeThinkingInSummary, undefined, conv.harness ?? undefined);\n summary = prependFallbackFocus(result.summary, options.focus, forkFallbackReason);\n usedSummaryModel = result.summaryModel;\n }\n } else {\n // Source authoring (legacy): deliver the handoff prompt to the live\n // source agent and wait for it to write the doc + .done sentinel.\n // Requires the source conversation to be alive and pollutes its\n // context with the handoff turn pair.\n const preconditionFallback = await handoffPreconditionFallbackReason(conv);\n if (preconditionFallback) {\n forkFallbackReason = preconditionFallback;\n effectiveForkMode = 'summary';\n logHandoffFallback(conv, preconditionFallback);\n const result = await generateSummarySeed(sourceSessionFile, summaryModel ?? undefined, options.localSummaryOnly, options.includeThinkingInSummary, undefined, conv.harness ?? undefined);\n summary = prependFallbackFocus(result.summary, options.focus, preconditionFallback);\n usedSummaryModel = result.summaryModel;\n } else {\n try {\n const handoff = await requestHandoffFromAgent(conv, options.focus, {\n timeoutMs: options.handoffTimeoutMs,\n pollIntervalMs: options.handoffPollIntervalMs,\n });\n summary = handoff.docText;\n usedSummaryModel = null;\n handoffDocPath = handoff.docPath;\n } catch (error) {\n forkFallbackReason = handoffFailureReason(error);\n effectiveForkMode = 'summary';\n logHandoffFallback(conv, forkFallbackReason);\n const result = await generateSummarySeed(sourceSessionFile, summaryModel ?? undefined, options.localSummaryOnly, options.includeThinkingInSummary, undefined, conv.harness ?? undefined);\n summary = prependFallbackFocus(result.summary, options.focus, forkFallbackReason);\n usedSummaryModel = result.summaryModel;\n }\n }\n }\n } else {\n const result = await generateSummarySeed(sourceSessionFile, summaryModel ?? undefined, options.localSummaryOnly, options.includeThinkingInSummary, undefined, conv.harness ?? undefined);\n summary = result.summary;\n usedSummaryModel = result.summaryModel;\n }\n\n const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');\n const suffix = randomUUID().slice(0, 4);\n const newName = `${timestamp}-${suffix}`;\n const newTmux = `conv-${newName}`;\n\n let newConv = createConversation({\n name: newName,\n tmuxSession: newTmux,\n cwd,\n issueId: conv.issueId ?? undefined,\n title: effectiveForkMode === 'plain'\n ? `Fork: ${conv.title || conv.name}`\n : effectiveForkMode === 'handoff'\n ? `Handoff: ${conv.title || conv.name}`\n : `Summary Fork: ${conv.title || conv.name}`,\n titleSource: 'manual',\n titleSeed: effectiveForkMode === 'plain'\n ? `Fork of ${conv.name}`\n : effectiveForkMode === 'handoff'\n ? `Handoff of ${conv.name}`\n : `Summary Fork of ${conv.name}`,\n claudeSessionId: sessionId,\n model: launchModel ?? undefined,\n effort: conv.effort ?? undefined,\n harness: options.harness ?? conv.harness ?? undefined,\n });\n if (handoffDocPath) {\n newConv = recordConversationHandoff(conv.name, newConv.name, handoffDocPath);\n }\n if (forkFallbackReason) {\n updateConversationForkFallbackReason(newConv.name, forkFallbackReason);\n newConv = getConversationByName(newConv.name) ?? newConv;\n }\n\n return {\n conversation: newConv,\n sessionId,\n sessionFile,\n summary,\n summaryModel: usedSummaryModel,\n forkMode: effectiveForkMode,\n handoffDocPath,\n forkFallbackReason,\n };\n}\n\n// Re-export runModelSummary for any callers that need it directly\nexport { runModelSummary };\n\n// ─── Effect variants (PAN-1249, additive) ────────────────────────────────────\n//\n// Additive Effect surface for fork helpers. Failures from the underlying\n// fs ops or LLM calls surface as FsError (filesystem failures) or Error\n// (LLM / generation failures). The existing Promise functions remain\n// canonical; these are wrappers for Effect-native callers.\n\n/** Effect variant of generateFallbackSummary. */\nexport function generateFallbackSummary(\n jsonlPath: string,\n): Effect.Effect<string, FsError> {\n return Effect.tryPromise({\n try: () => generateFallbackSummaryPromise(jsonlPath),\n catch: (cause) =>\n new FsError({ path: jsonlPath, operation: 'fallback-summary', cause }),\n });\n}\n\n/** Effect variant of reserveSummaryForkSession. */\nexport function reserveSummaryForkSession(\n cwd: string,\n): Effect.Effect<{ sessionId: string; sessionFile: string }, FsError> {\n return Effect.tryPromise({\n try: () => reserveSummaryForkSessionPromise(cwd),\n catch: (cause) =>\n new FsError({ path: cwd, operation: 'reserve-session', cause }),\n });\n}\n\n/** Effect variant of copySessionFromCompactBoundary. */\nexport function copySessionFromCompactBoundary(\n sourcePath: string,\n destPath: string,\n): Effect.Effect<void, FsError> {\n return Effect.tryPromise({\n try: () => copySessionFromCompactBoundaryPromise(sourcePath, destPath),\n catch: (cause) =>\n new FsError({ path: sourcePath, operation: 'copy-session', cause }),\n });\n}\n\n/** Effect variant of createSummaryFork. */\nexport function createSummaryFork(\n conv: Conversation,\n options: SummaryForkOptions = {},\n): Effect.Effect<SummaryForkResult, FsError> {\n return Effect.tryPromise({\n try: () => createSummaryForkPromise(conv, options),\n catch: (cause) =>\n new FsError({ path: conv.name, operation: 'create-summary-fork', cause }),\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;aA6B8C;wBAMf;;;;;aCjCc;;;;AC0b7C,eAAe,+BAA+B,WAAoC;CAChF,MAAM,EAAE,aAAa,MAAM,OAAO;CAClC,MAAM,SAAS,MAAM,SAAS,WAAW,QAAQ,EAC9C,MAAM,KAAK,CACX,QAAQ,MAAM,EAAE,MAAM,CAAC;CAE1B,MAAM,eAAyB,EAAE;CACjC,MAAM,gCAAgB,IAAI,KAAa;CACvC,MAAM,4BAAY,IAAI,KAAa;AAEnC,MAAK,MAAM,QAAQ,OAAO;EACxB,IAAI;AACJ,MAAI;AACF,WAAQ,KAAK,MAAM,KAAK;UAClB;AACN;;AAGF,MAAI,MAAM,SAAS,UAAU,MAAM,SAAS;GAC1C,MAAM,UAAU,MAAM,QAAQ;AAC9B,OAAI,OAAO,YAAY,YAAY,QAAQ,MAAM;QAC3C,CAAC,QAAQ,MAAM,CAAC,WAAW,iBAAiB,IAAI,CAAC,QAAQ,MAAM,CAAC,WAAW,gBAAgB,CAC7F,cAAa,KAAK,QAAQ,MAAM,CAAC;cAE1B,MAAM,QAAQ,QAAQ;SAC1B,MAAM,SAAS,QAClB,KAAI,MAAM,SAAS,UAAU,MAAM,MAAM,MAAM,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,WAAW,IAAI,CACnF,cAAa,KAAK,MAAM,KAAK,MAAM,CAAC;;;AAM5C,MAAI,MAAM,SAAS,eAAe,MAAM,SAAS,SAAS;GACxD,MAAM,UAAU,MAAM,QAAQ;AAC9B,OAAI,MAAM,QAAQ,QAAQ;SACnB,MAAM,SAAS,QAClB,KAAI,MAAM,SAAS,YAAY;AAC7B,eAAU,IAAI,MAAM,KAAK;AACzB,SAAI,MAAM,SAAS,UAAU,MAAM,SAAS,SAAS;MACnD,MAAM,KAAK,MAAM,OAAO,aAAa,MAAM,OAAO;AAClD,UAAI,GAAI,eAAc,IAAI,GAAG;;;;;;CAQzC,IAAI,UAAU;AACd,YAAW;AAEX,KAAI,aAAa,SAAS,GAAG;AAC3B,aAAW;AACX,OAAK,MAAM,OAAO,aAAa,MAAM,GAAG,GAAG,CACzC,YAAW,KAAK,IAAI,MAAM,GAAG,IAAI,GAAG,IAAI,SAAS,MAAM,QAAQ,GAAG;AAEpE,aAAW;;AAGb,KAAI,cAAc,OAAO,GAAG;AAC1B,aAAW;AACX,OAAK,MAAM,KAAK,CAAC,GAAG,cAAc,CAAC,MAAM,CACvC,YAAW,OAAO,EAAE,QAAQ,kBAAkB,GAAG,CAAC;AAEpD,aAAW;;AAGb,KAAI,UAAU,OAAO,EACnB,YAAW,mBAAmB,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,KAAK,CAAC;AAGjE,YAAW;AAEX,QAAO;;;AAiNT,SAAgB,wBACd,WACgC;AAChC,QAAO,OAAO,WAAW;EACvB,WAAW,+BAA+B,UAAU;EACpD,QAAQ,UACN,IAAI,QAAQ;GAAE,MAAM;GAAW,WAAW;GAAoB;GAAO,CAAC;EACzE,CAAC;;;;qBA5rBkC;aAC6C;mBAChC;cACA;wBACK;0BACO;qBAC+B;cAEvD;oBACgC;AA6CjE,yBAAwB"}
1
+ {"version":3,"file":"summary-fork-Dx1RMuvU.js","names":[],"sources":["../src/lib/conversations/transcript-adapter.ts","../src/lib/conversations/handoff-paths.ts","../src/lib/conversations/summary-fork.ts"],"sourcesContent":["/**\n * Conversation transcript adapter.\n *\n * Each agent harness writes its session transcript in its own JSONL shape:\n *\n * - Claude Code records have top-level `type: 'user'|'assistant'|'system'`,\n * with `entry.message.content` as a block array whose blocks are\n * `text|tool_use|tool_result|thinking`.\n * - Pi records have top-level `type: 'message'|'session'|'model_change'|\n * 'thinking_level_change'|...`, with the user/assistant role nested in\n * `entry.message.role` and blocks of type `text|thinking|toolCall|toolResult`.\n * - Future harnesses (Codex, etc.) will have their own shapes.\n *\n * The handoff authoring pipeline doesn't care about any of that. It needs\n * one thing from each harness: a canonical \"<conversation>...</conversation>\"\n * text it can feed the authoring model.\n *\n * This module provides that abstraction. Adding a new harness is two short\n * functions (resolveSessionFile + serialize) plus a registry entry — the\n * fork pipeline never needs to learn about the new harness.\n */\nimport { existsSync } from 'node:fs';\nimport { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { Effect } from 'effect';\n\nimport type { LegacyConversation as Conversation } from '../overdeck/conversations.js';\nimport type { RuntimeName } from '../runtimes/types.js';\nimport { sessionFilePath } from '../paths.js';\nimport {\n parseEntries as parseClaudeCodeEntries,\n serializeConversation as serializeClaudeCodeConversation,\n generateSmartSummary,\n summarizeSerializedText,\n} from './smart-compaction.js';\n\nexport interface CompactSummaryOptions {\n /** Model for the summarizer LLM. */\n model?: string;\n /** Use the rich (Claude-native-style) summary prompt variant. */\n richMode?: boolean;\n /** Include thinking blocks in the serialized source. Default: true. */\n includeThinking?: boolean;\n /** Harness backend the summarizer LLM runs on. Default: 'claude-code'. */\n harness?: RuntimeName;\n /** Per-LLM-call timeout (ms). */\n timeoutMs?: number;\n}\n\nexport interface ConversationTranscriptAdapter {\n /** Display name for logging/errors. */\n readonly name: RuntimeName;\n\n /** Plain forks copy raw Claude JSONL and spawn Claude with `--resume` —\n * only Claude Code can be the source of a plain fork. */\n readonly supportsPlainForkAsSource: boolean;\n\n /** Source-authored handoff requires delivering a prompt to the live source\n * agent and waiting for it to write a sentinel file. Claude Code supports\n * this via deliverAgentMessage; Pi has no hook-equivalent signaling channel\n * for the .done sentinel (see PAN-1134), so source authoring is gated off\n * for Pi until that lands. */\n readonly supportsSourceAuthoredHandoff: boolean;\n\n /** Resolve the path to the source conversation's transcript file.\n * Returns null if the conversation has no transcript yet (e.g. session\n * just started and hasn't produced a JSONL line). */\n resolveSessionFile(conv: Conversation): Promise<string | null>;\n\n /** Serialize the transcript into a harness-agnostic canonical text that\n * the handoff prompt template can embed verbatim. Each turn is rendered\n * with a `[user]` / `[assistant]` header followed by text and tool-use\n * summaries; thinking blocks are optional (defaults to including them).\n */\n serializeTranscript(sessionFile: string, options?: { includeThinking?: boolean }): Promise<string>;\n\n /**\n * Produce a model-generated compact summary of the transcript, suitable for\n * seeding a summary fork or feeding the handoff authoring prompt.\n *\n * claude-code uses the entry-aware smart-compaction flow (rich, file-op\n * aware); other harnesses serialize the transcript and run a generic\n * chunk-and-merge summarizer over the text. The `harness` option selects the\n * summarizer LLM backend and is independent of which adapter (= source\n * harness) produced the transcript.\n */\n compactSummary(\n sessionFile: string,\n options?: CompactSummaryOptions,\n ): Promise<{ summary: string; summaryModel: string | null }>;\n}\n\n// ─── Claude Code ──────────────────────────────────────────────────────────\n\nconst claudeCodeAdapter: ConversationTranscriptAdapter = {\n name: 'claude-code',\n supportsPlainForkAsSource: true,\n supportsSourceAuthoredHandoff: true,\n\n async resolveSessionFile(conv) {\n if (!conv.claudeSessionId) return null;\n const path = sessionFilePath(conv.cwd, conv.claudeSessionId);\n return existsSync(path) ? path : null;\n },\n\n async serializeTranscript(sessionFile, options) {\n const entries = await parseClaudeCodeEntries(sessionFile);\n return serializeClaudeCodeConversation(entries, options?.includeThinking ?? true);\n },\n\n async compactSummary(sessionFile, options) {\n // Claude Code keeps the entry-aware smart-compaction flow: it parses the\n // JSONL into typed entries, finds compact boundaries, and carries file-op\n // detail into the summary. This is richer than text-only chunking.\n const result = await Effect.runPromise(\n generateSmartSummary({\n jsonlPath: sessionFile,\n model: options?.model,\n richMode: options?.richMode ?? false,\n mode: 'fork',\n includeThinkingInSummary: options?.includeThinking ?? true,\n harness: options?.harness ?? 'claude-code',\n }),\n );\n return { summary: result.summary, summaryModel: result.summaryModel };\n },\n};\n\n// ─── Pi ───────────────────────────────────────────────────────────────────\n\n/**\n * Pi sessions live at:\n * `~/.overdeck/agents/<tmuxSession>/sessions/<iso-timestamp>_<id>.jsonl`\n *\n * Pi may rotate session files (e.g. on resume), so we pick the\n * newest by filename — filenames sort lexicographically by their\n * ISO timestamp prefix so this is deterministic.\n */\nasync function resolvePiSessionFileFromTmux(tmuxSession: string): Promise<string | null> {\n const sessionDir = join(homedir(), '.overdeck', 'agents', tmuxSession, 'sessions');\n if (!existsSync(sessionDir)) return null;\n try {\n const entries = (await readdir(sessionDir)).filter((name) => name.endsWith('.jsonl'));\n if (entries.length === 0) return null;\n entries.sort();\n return join(sessionDir, entries[entries.length - 1]!);\n } catch {\n return null;\n }\n}\n\ninterface PiEntry {\n type?: string;\n message?: {\n role?: string;\n content?: Array<{\n type?: string;\n text?: string;\n thinking?: string;\n name?: string;\n arguments?: unknown;\n }>;\n };\n}\n\nfunction serializePiEntry(entry: PiEntry, includeThinking: boolean): string | undefined {\n if (entry.type !== 'message') return undefined;\n const role = entry.message?.role;\n if (role !== 'user' && role !== 'assistant') return undefined;\n\n const content = entry.message?.content;\n if (!Array.isArray(content)) return undefined;\n\n const lines: string[] = [`[${role}]`];\n for (const block of content) {\n if (block?.type === 'text' && typeof block.text === 'string' && block.text.trim()) {\n lines.push(block.text);\n } else if (block?.type === 'thinking' && includeThinking && typeof block.thinking === 'string' && block.thinking.trim()) {\n lines.push(`[thinking]\\n${block.thinking}`);\n } else if (block?.type === 'toolCall' && typeof block.name === 'string') {\n let args = '';\n try {\n args = JSON.stringify(block.arguments).slice(0, 500);\n } catch {\n args = '<unserializable>';\n }\n lines.push(`[tool_use: ${block.name}]\\n${args}`);\n }\n // toolResult, function-call envelopes, etc. are intentionally skipped — the\n // surface we care about for handoff is what the user said, what the agent\n // said, what tools it ran, and what it was thinking. Tool outputs balloon\n // the transcript without adding signal a handoff doc needs.\n }\n if (lines.length === 1) return undefined; // header only — no content\n return lines.join('\\n');\n}\n\nconst piAdapter: ConversationTranscriptAdapter = {\n name: 'ohmypi',\n supportsPlainForkAsSource: false,\n supportsSourceAuthoredHandoff: false,\n\n async resolveSessionFile(conv) {\n return resolvePiSessionFileFromTmux(conv.tmuxSession);\n },\n\n async serializeTranscript(sessionFile, options) {\n const includeThinking = options?.includeThinking ?? true;\n const content = await readFile(sessionFile, 'utf-8');\n const parts: string[] = [];\n for (const line of content.split('\\n')) {\n if (!line.trim()) continue;\n let entry: PiEntry;\n try {\n entry = JSON.parse(line) as PiEntry;\n } catch {\n continue;\n }\n const serialized = serializePiEntry(entry, includeThinking);\n if (serialized) parts.push(serialized);\n }\n return parts.join('\\n\\n');\n },\n\n async compactSummary(sessionFile, options) {\n // Pi's JSONL shape is not what the Claude-Code entry parser understands, so\n // we serialize the transcript through this adapter first, then run the\n // generic text chunk-and-merge summarizer over it.\n const serialized = await piAdapter.serializeTranscript(sessionFile, {\n includeThinking: options?.includeThinking ?? true,\n });\n if (!serialized.trim()) {\n return { summary: '', summaryModel: null };\n }\n const summary = await summarizeSerializedText(serialized, {\n model: options?.model,\n richMode: options?.richMode ?? false,\n harness: options?.harness ?? 'claude-code',\n timeoutMs: options?.timeoutMs,\n });\n return { summary, summaryModel: options?.model ?? null };\n },\n};\n\n// ─── Registry ─────────────────────────────────────────────────────────────\n\nconst REGISTRY: Record<RuntimeName, ConversationTranscriptAdapter> = {\n 'claude-code': claudeCodeAdapter,\n 'ohmypi': piAdapter,\n 'codex': claudeCodeAdapter,\n};\n\n/**\n * Look up the transcript adapter for a harness. Unknown harnesses default to\n * the Claude Code adapter — that matches the conservative behavior of the rest\n * of the codebase (see getHarness() in @overdeck/contracts).\n */\nexport function getTranscriptAdapter(harness: RuntimeName | undefined): ConversationTranscriptAdapter {\n return REGISTRY[harness ?? 'claude-code'] ?? claudeCodeAdapter;\n}\n","import { mkdir } from 'fs/promises';\nimport { join } from 'path';\nimport { getHandoffsDir } from '../paths.js';\n\nconst SAFE_PATH_SEGMENT = /^[A-Za-z0-9._-]+$/;\n\nexport interface HandoffPaths {\n docPath: string;\n sentinelPath: string;\n}\n\nexport function createHandoffPaths(sourceConvId: string, isoTimestamp: string): HandoffPaths {\n const convId = sourceConvId.trim();\n const timestamp = isoTimestamp.trim();\n\n if (!SAFE_PATH_SEGMENT.test(convId)) {\n throw new Error(`Invalid handoff conversation id: ${sourceConvId}`);\n }\n if (timestamp.length === 0 || timestamp.includes('/') || timestamp.includes('\\\\')) {\n throw new Error(`Invalid handoff timestamp: ${isoTimestamp}`);\n }\n\n const docPath = join(getHandoffsDir(), `${convId}-${timestamp}.md`);\n return {\n docPath,\n sentinelPath: `${docPath}.done`,\n };\n}\n\nexport async function ensureHandoffsDir(): Promise<string> {\n const handoffsDir = getHandoffsDir();\n await mkdir(handoffsDir, { recursive: true });\n return handoffsDir;\n}\n","/**\n * Conversation fork pipeline.\n *\n * This module handles creating a new conversation from an existing one.\n * Two modes are supported:\n *\n * 1. Summary fork (default): The conversation history is serialized and sent\n * to an LLM summarizer (see smart-compaction.ts). The generated structured\n * summary is injected as the first user message in the new session.\n *\n * 2. Plain fork: Raw JSONL history is copied from the last compact_boundary\n * into a new session file. Thinking blocks are sanitized (converted to text)\n * to prevent signature validation errors on cross-model resumes.\n *\n * Entry point: createSummaryFork()\n * - Reserves a new session ID and file path\n * - Generates summary (LLM, heuristic fallback, or skips for plain fork)\n * - Creates a DB record for the new conversation\n * - Returns the new conversation + session metadata\n *\n * Dashboard API: runForkPipeline() in src/dashboard/server/routes/conversations.ts\n * wires the options through and handles tmux spawn + summary injection.\n */\nimport { randomUUID } from 'node:crypto';\nimport { access, mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { Effect } from 'effect';\n\nimport type { LegacyConversation as Conversation } from '../overdeck/conversations.js';\nimport {\n createConversation,\n getConversationByName,\n recordConversationHandoff,\n updateConversationForkFallbackReason,\n} from '../overdeck/conversations.js';\nimport { encodeClaudeProjectDir, packageRoot, sessionFilePath } from '../paths.js';\nimport { loadConfigSync } from '../config-yaml.js';\nimport { deliverAgentMessage } from '../agents.js';\nimport { runModelSummary } from './smart-compaction.js';\nimport { getTranscriptAdapter } from './transcript-adapter.js';\nimport { createHandoffPaths, ensureHandoffsDir, type HandoffPaths } from './handoff-paths.js';\nimport type { RuntimeName } from '../runtimes/types.js';\nimport { FsError } from '../errors.js';\nimport { getWorkspaceStackHealth } from '../workspace/stack-health.js';\n\nexport type SummaryForkMode = 'summary' | 'plain' | 'handoff';\nexport type HandoffAuthor = 'source' | 'external';\n\nexport interface SummaryForkOptions {\n model?: string;\n cwd?: string;\n harness?: RuntimeName;\n localSummaryOnly?: boolean;\n forkMode?: SummaryForkMode;\n focus?: string;\n handoffTimeoutMs?: number;\n handoffPollIntervalMs?: number;\n /** When true, include thinking block content in the serialized conversation sent to the summary model. Default: true. */\n includeThinkingInSummary?: boolean;\n /**\n * Who authors the handoff document when forkMode === 'handoff'.\n *\n * - 'source' (legacy): deliver the handoff prompt to the live source agent\n * via deliverAgentMessage and wait for it to write the doc + .done sentinel.\n * Requires the source conversation to be alive. Pollutes the source's\n * context. Uses whatever model/harness the source is running on.\n * - 'external' (default): spawn a separate authoring session with the chosen\n * model/harness that reads the source JSONL transcript and emits the doc.\n * Does not touch the source conversation. Works on ended sources.\n */\n handoffAuthor?: HandoffAuthor;\n /** Model for the external handoff authoring session. Ignored when handoffAuthor === 'source'. */\n handoffAuthorModel?: string;\n /** Harness for the external handoff authoring session. Ignored when handoffAuthor === 'source'. */\n handoffAuthorHarness?: RuntimeName;\n}\n\nexport interface SummaryForkResult {\n conversation: Conversation;\n sessionId: string;\n sessionFile: string;\n summary: string;\n summaryModel: string | null;\n forkMode: SummaryForkMode;\n handoffDocPath: string | null;\n forkFallbackReason: string | null;\n}\n\nconst FORK_WAIT_INSTRUCTION = `\\n---\\n\\n**Do not take any action.** This is context from a prior conversation fork. Acknowledge the summary and wait for the user's next instruction.`;\nconst DEFAULT_HANDOFF_TIMEOUT_MS = 300_000;\nconst DEFAULT_HANDOFF_POLL_INTERVAL_MS = 1_000;\nconst NO_HANDOFF_FOCUS = 'No specific focus was provided.';\n\nexport type HandoffDocValidation =\n | { ok: true }\n | { ok: false; reason: string };\n\nexport interface RequestHandoffOptions {\n timeoutMs?: number;\n pollIntervalMs?: number;\n now?: Date;\n}\n\nexport interface RequestHandoffResult {\n docPath: string;\n docText: string;\n}\n\nexport class HandoffStallError extends Error {\n constructor(\n public readonly docPath: string,\n public readonly sentinelPath: string,\n public readonly timeoutMs: number,\n ) {\n super(`Timed out waiting ${timeoutMs}ms for handoff document and sentinel: ${docPath}, ${sentinelPath}`);\n this.name = 'HandoffStallError';\n }\n}\n\nexport class HandoffValidationError extends Error {\n constructor(\n public readonly docPath: string,\n public readonly reason: string,\n ) {\n super(`Invalid handoff document ${docPath}: ${reason}`);\n this.name = 'HandoffValidationError';\n }\n}\n\n/**\n * Strip a wrapping ``` fenced code block from the doc body if the LLM\n * helpfully wrapped its Markdown output in a fence. Returns the inner\n * content if a fence was detected, otherwise returns the trimmed input.\n */\nexport function sanitizeHandoffDoc(text: string): string {\n const trimmed = text.trim();\n const fenceMatch = trimmed.match(/^```(?:markdown|md)?\\s*\\n([\\s\\S]*?)\\n```\\s*$/);\n return fenceMatch?.[1]?.trim() ?? trimmed;\n}\n\nexport function validateHandoffDoc(text: string): HandoffDocValidation {\n const sanitized = sanitizeHandoffDoc(text);\n if (sanitized.length < 200) {\n return { ok: false, reason: 'handoff document must be at least 200 characters' };\n }\n // Accept any heading depth (H1-H6), case-insensitive, with an optional\n // trailing colon. Real-world LLM outputs vary on heading conventions; the\n // failure mode of a too-strict validator is silent fallback to summary fork\n // with no surface to the user, which is the worst outcome.\n if (!/^#{1,6}\\s+suggested skills\\s*:?\\s*$/imu.test(sanitized)) {\n return { ok: false, reason: 'handoff document must contain a Suggested skills heading' };\n }\n return { ok: true };\n}\n\nfunction renderHandoffPrompt(template: string, focus: string | undefined, outputPath: string): string {\n const safeFocus = focus?.trim() || NO_HANDOFF_FOCUS;\n return template\n .split('{{focus}}').join(safeFocus)\n .split('{{outputPath}}').join(outputPath);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function waitForHandoffDoc(paths: HandoffPaths, timeoutMs: number, pollIntervalMs: number): Promise<string> {\n const deadline = Date.now() + timeoutMs;\n\n while (true) {\n try {\n await access(paths.docPath);\n await access(paths.sentinelPath);\n return await readFile(paths.docPath, 'utf-8');\n } catch {\n const remainingMs = deadline - Date.now();\n if (remainingMs <= 0) {\n throw new HandoffStallError(paths.docPath, paths.sentinelPath, timeoutMs);\n }\n await delay(Math.min(pollIntervalMs, remainingMs));\n }\n }\n}\n\nconst DEFAULT_HANDOFF_AUTHOR_MODEL = 'claude-sonnet-4-6';\nconst HANDOFF_AUTHOR_TIMEOUT_MS = 300_000;\n// When the raw transcript exceeds this many characters, pre-compact it via\n// generateSmartSummary (which chunks internally) before sending to the\n// handoff authoring model. Threshold chosen to keep the final prompt\n// comfortably under Haiku's 200k-token window (~800k chars at 4 chars/token,\n// minus headroom for the template, focus, and a response budget). The exact\n// value isn't critical — anything that triggers compaction for \"long\"\n// transcripts is fine.\nconst HANDOFF_TRANSCRIPT_COMPACT_THRESHOLD = 100_000;\n\nfunction renderExternalHandoffPrompt(template: string, focus: string | undefined, transcript: string, outputPath: string): string {\n const safeFocus = focus?.trim() || NO_HANDOFF_FOCUS;\n return template\n .split('{{focus}}').join(safeFocus)\n .split('{{outputPath}}').join(outputPath)\n .split('{{transcript}}').join(transcript);\n}\n\n/**\n * Author a handoff document from an external authoring session.\n *\n * Reads the source JSONL transcript and asks a fresh, isolated model session\n * (with the chosen model + harness) to write the handoff Markdown. The source\n * conversation is never contacted — its context is not polluted.\n *\n * Returns the same { docPath, docText } shape as requestHandoffFromAgent so\n * callers can substitute one for the other.\n */\nexport async function authorHandoffExternal(\n sourceConv: Conversation,\n sourceSessionFile: string,\n focus: string | undefined,\n model: string | undefined,\n harness: RuntimeName | undefined,\n options: { now?: Date } = {},\n): Promise<RequestHandoffResult> {\n await ensureHandoffsDir();\n const timestamp = (options.now ?? new Date()).toISOString();\n const paths = createHandoffPaths(sourceConv.name, timestamp);\n\n const effectiveModel = model ?? DEFAULT_HANDOFF_AUTHOR_MODEL;\n const effectiveHarness: RuntimeName = harness ?? 'claude-code';\n\n // The authoring harness decides the prompt template: Claude Code's `Write`\n // tool vs Pi's lowercase `write` tool, with harness-specific phrasing about\n // how the model should call it. Both templates share the same H2 contract,\n // and the file-on-disk read below is harness-independent.\n const templateName = effectiveHarness === 'ohmypi' ? 'handoff-external-pi.md' : 'handoff-external.md';\n const template = await readFile(join(packageRoot, 'roles', templateName), 'utf-8');\n\n // The source's harness decides how the transcript is read/serialized; the\n // authoring harness (model + harness picked by the user) is independent.\n const sourceAdapter = getTranscriptAdapter(sourceConv.harness ?? undefined);\n // Skip thinking blocks — they're large and the structured output we want\n // doesn't need internal reasoning, only the user/assistant exchange.\n const transcript = await sourceAdapter.serializeTranscript(sourceSessionFile, { includeThinking: false });\n\n // If the raw transcript is small enough, feed it to the model verbatim —\n // that's the richest input. For long transcripts we'd overflow any model's\n // context window in a single-pass prompt, so first compact via the chunked\n // generateSmartSummary path (which already handles chunking + merging) and\n // feed the compact summary to the handoff authoring step. The handoff\n // template treats {{transcript}} as opaque context — a compact summary\n // works as well as the raw transcript, just with less fine detail.\n let promptInput: string;\n let inputLabel: 'raw-transcript' | 'compact-summary';\n // Precompaction goes through the source adapter's compactSummary, which is\n // harness-agnostic: claude-code uses the entry-aware smart-compaction flow,\n // Pi (and future harnesses) serialize their transcript and run the generic\n // chunk-and-merge summarizer. Either way a long source transcript is\n // compacted before it reaches the authoring model's context window.\n if (transcript.length > HANDOFF_TRANSCRIPT_COMPACT_THRESHOLD) {\n console.log(`[claude-invoke] purpose=handoff-author-external | model=${effectiveModel} | harness=${effectiveHarness} | source=${sourceConv.name} | transcriptChars=${transcript.length} | precompacting=true`);\n const { config } = loadConfigSync();\n const richMode = config.conversations.richCompaction;\n const compact = await sourceAdapter.compactSummary(sourceSessionFile, {\n model: effectiveModel,\n richMode,\n includeThinking: false,\n harness: effectiveHarness,\n });\n promptInput = compact.summary;\n inputLabel = 'compact-summary';\n console.log(`[claude-invoke] purpose=handoff-author-external precompact-result | model=${effectiveModel} | compactChars=${compact.summary.length}`);\n } else {\n promptInput = transcript;\n inputLabel = 'raw-transcript';\n }\n\n const prompt = renderExternalHandoffPrompt(template, focus, promptInput, paths.docPath);\n console.log(`[claude-invoke] purpose=handoff-author-external | model=${effectiveModel} | harness=${effectiveHarness} | source=${sourceConv.name} | inputType=${inputLabel} | promptChars=${prompt.length} | outputPath=${paths.docPath}`);\n\n // The prompt tells the model to use its Write tool to author the document\n // directly at paths.docPath. We capture stdout only as a diagnostic log —\n // the source of truth for the doc is the file on disk. Writing via tool\n // avoids stdout preamble leaks like \"Here is the handoff document:\" that\n // contaminated earlier attempts.\n //\n // The authoring session is headless (`claude -p`), so the Write tool must be\n // explicitly allowlisted — otherwise `--permission-mode auto` raises a\n // permission prompt the session can never answer, the model emits \"I need\n // permission to write…\" to stdout, no file is written, and the whole fork\n // falls back to a plain summary with reason `handoff-validation` (PAN-1582).\n // Pi runs in rpc mode and auto-executes tools, so the allowlist is a no-op there.\n const stdout = await Effect.runPromise(runModelSummary(prompt, effectiveModel, HANDOFF_AUTHOR_TIMEOUT_MS, effectiveHarness, ['Write']));\n console.log(`[claude-invoke] purpose=handoff-author-external acknowledgement | model=${effectiveModel} | stdoutChars=${stdout.length} | stdoutPreview=${JSON.stringify(stdout.slice(0, 120))}`);\n\n let docText: string;\n try {\n docText = await readFile(paths.docPath, 'utf-8');\n } catch (err) {\n // The model didn't use Write — it almost certainly produced the doc on\n // stdout instead. Persist whatever we got for diagnosis, then surface\n // the failure as a validation error so the pipeline falls back to a\n // summary fork rather than spinning forever.\n const rejectedPath = `${paths.docPath}.rejected.md`;\n await writeFile(rejectedPath, stdout, 'utf-8').catch((wErr) => {\n console.warn(`[handoff-author-external] failed to persist stdout to ${rejectedPath}: ${wErr?.message ?? wErr}`);\n });\n console.warn(`[handoff-author-external] model did not call Write — stdout (${stdout.length} chars) saved to ${rejectedPath}: ${(err as { message?: string })?.message ?? err}`);\n throw new HandoffValidationError(paths.docPath, 'authoring session did not call its Write tool to create the handoff doc');\n }\n\n const validation = validateHandoffDoc(docText);\n if (!validation.ok) {\n // The file exists but its content failed the contract check. Persist\n // the rejected file for diagnosis and surface the reason.\n const rejectedPath = `${paths.docPath}.rejected.md`;\n await writeFile(rejectedPath, docText, 'utf-8').catch((err) => {\n console.warn(`[handoff-author-external] failed to persist rejected doc to ${rejectedPath}: ${err?.message ?? err}`);\n });\n console.warn(`[handoff-author-external] validation rejected file content (${validation.reason}); copy saved to ${rejectedPath}`);\n throw new HandoffValidationError(paths.docPath, validation.reason);\n }\n\n // Sanitize as a safety net in case the model wrapped the file in fences\n // despite the prompt telling it not to. Overwrite the file with the\n // cleaned text so downstream consumers see the same thing the validator\n // approved.\n const sanitized = sanitizeHandoffDoc(docText);\n if (sanitized !== docText) {\n await writeFile(paths.docPath, sanitized, 'utf-8');\n }\n await writeFile(paths.sentinelPath, '', 'utf-8');\n\n return { docPath: paths.docPath, docText: sanitized };\n}\n\nexport async function requestHandoffFromAgent(\n sourceConv: Conversation,\n focus?: string,\n options: RequestHandoffOptions = {},\n): Promise<RequestHandoffResult> {\n await ensureHandoffsDir();\n const timestamp = (options.now ?? new Date()).toISOString();\n const paths = createHandoffPaths(sourceConv.name, timestamp);\n const template = await readFile(join(packageRoot, 'roles', 'handoff.md'), 'utf-8');\n const prompt = renderHandoffPrompt(template, focus, paths.docPath);\n\n await deliverAgentMessage(sourceConv.tmuxSession, prompt, 'handoff-request');\n\n const docText = await waitForHandoffDoc(\n paths,\n options.timeoutMs ?? DEFAULT_HANDOFF_TIMEOUT_MS,\n options.pollIntervalMs ?? DEFAULT_HANDOFF_POLL_INTERVAL_MS,\n );\n const validation = validateHandoffDoc(docText);\n if (!validation.ok) {\n // The source agent already wrote the file to docPath. Move it aside to\n // .rejected.md so the next handoff attempt doesn't reuse a stale invalid\n // doc and so the operator can inspect what the agent wrote.\n const rejectedPath = `${paths.docPath}.rejected.md`;\n await writeFile(rejectedPath, docText, 'utf-8').catch((err) => {\n console.warn(`[handoff-source] failed to persist rejected output to ${rejectedPath}: ${err?.message ?? err}`);\n });\n console.warn(`[handoff-source] validation rejected source-authored doc (${validation.reason}); raw output saved to ${rejectedPath}`);\n throw new HandoffValidationError(paths.docPath, validation.reason);\n }\n\n return { docPath: paths.docPath, docText: sanitizeHandoffDoc(docText) };\n}\n\nfunction workspaceSourceFromCwd(sourceConv: Conversation): { issueId: string; workspacePath: string } | null {\n const normalizedCwd = sourceConv.cwd.replace(/\\\\/g, '/');\n const match = normalizedCwd.match(/^((?:(?:.*)\\/)?workspaces\\/feature-([a-z]+-\\d+))(?:\\/.*)?$/i);\n if (!match?.[1] || !match[2]) return null;\n return {\n issueId: sourceConv.issueId ?? match[2].toUpperCase(),\n workspacePath: match[1],\n };\n}\n\nexport async function handoffPreconditionFallbackReason(sourceConv: Conversation): Promise<string | null> {\n if (sourceConv.status === 'ended') return 'source-ended';\n\n const workspaceSource = workspaceSourceFromCwd(sourceConv);\n if (!workspaceSource) return null;\n\n try {\n await access(join(workspaceSource.workspacePath, '.devcontainer'));\n } catch {\n return null;\n }\n\n const health = await Effect.runPromise(getWorkspaceStackHealth(workspaceSource.issueId, {\n workspacePath: workspaceSource.workspacePath,\n }));\n return health.healthy ? 'source-workspace-devcontainer' : null;\n}\n\nexport function handoffFailureReason(error: unknown): string {\n if (error instanceof HandoffStallError) return 'handoff-timeout';\n if (error instanceof HandoffValidationError) return 'handoff-validation';\n return 'handoff-request-failed';\n}\n\nexport function logHandoffFallback(sourceConv: Conversation, reason: string): void {\n console.warn(`[summary-fork] handoff-fallback source=${sourceConv.name} reason=${reason}`);\n}\n\n/**\n * When a handoff falls back to a summary fork, the user's focus text would\n * otherwise be silently dropped. Prepend a small notice to the summary so the\n * successor conversation still sees what was asked, and so the user gets a\n * visible breadcrumb that the intended handoff failed.\n */\nexport function prependFallbackFocus(summary: string, focus: string | undefined, fallbackReason: string): string {\n const trimmedFocus = focus?.trim();\n if (!trimmedFocus) return summary;\n const header = [\n `**Note from Overdeck:** the intended handoff fell back to a summary fork (\\`${fallbackReason}\\`). The focus you requested is preserved below; the summary that follows is auto-generated, not an authored handoff document.`,\n '',\n '**Requested focus:**',\n '',\n `> ${trimmedFocus.split('\\n').join('\\n> ')}`,\n '',\n '---',\n '',\n ].join('\\n');\n return header + summary;\n}\n\nasync function generateSummarySeed(\n sourceSessionFile: string,\n summaryModel: string | undefined,\n localSummaryOnly: boolean | undefined,\n includeThinkingInSummary: boolean | undefined,\n summaryHarness?: RuntimeName,\n sourceHarness?: RuntimeName,\n): Promise<{ summary: string; summaryModel: string | null }> {\n if (localSummaryOnly) {\n return {\n summary: await Effect.runPromise(generateFallbackSummary(sourceSessionFile)),\n summaryModel: null,\n };\n }\n return generateSummaryForFork(sourceSessionFile, summaryModel, includeThinkingInSummary, summaryHarness, sourceHarness);\n}\n\nasync function generateFallbackSummaryPromise(jsonlPath: string): Promise<string> {\n const { readFile } = await import('node:fs/promises');\n const lines = (await readFile(jsonlPath, 'utf-8'))\n .split('\\n')\n .filter((l) => l.trim());\n\n const userMessages: string[] = [];\n const filesModified = new Set<string>();\n const toolsUsed = new Set<string>();\n\n for (const line of lines) {\n let entry: any;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n\n if (entry.type === 'user' && entry.message) {\n const content = entry.message.content;\n if (typeof content === 'string' && content.trim()) {\n if (!content.trim().startsWith('<local-command') && !content.trim().startsWith('<command-name')) {\n userMessages.push(content.trim());\n }\n } else if (Array.isArray(content)) {\n for (const block of content) {\n if (block.type === 'text' && block.text?.trim() && !block.text.trim().startsWith('<')) {\n userMessages.push(block.text.trim());\n }\n }\n }\n }\n\n if (entry.type === 'assistant' && entry.message?.content) {\n const content = entry.message.content;\n if (Array.isArray(content)) {\n for (const block of content) {\n if (block.type === 'tool_use') {\n toolsUsed.add(block.name);\n if (block.name === 'Edit' || block.name === 'Write') {\n const fp = block.input?.file_path || block.input?.path;\n if (fp) filesModified.add(fp);\n }\n }\n }\n }\n }\n }\n\n let summary = `## Conversation Summary Fork\\n\\n`;\n summary += `This is a continuation of a previous conversation, seeded with a summary of the earlier work.\\n\\n`;\n\n if (userMessages.length > 0) {\n summary += `### User Messages:\\n`;\n for (const msg of userMessages.slice(0, 10)) {\n summary += `- ${msg.slice(0, 200)}${msg.length > 200 ? '...' : ''}\\n`;\n }\n summary += '\\n';\n }\n\n if (filesModified.size > 0) {\n summary += `### Files Modified:\\n`;\n for (const f of [...filesModified].sort()) {\n summary += `- \\`${f.replace(/.*\\/overdeck\\//, '')}\\`\\n`;\n }\n summary += '\\n';\n }\n\n if (toolsUsed.size > 0) {\n summary += `### Tools Used: ${[...toolsUsed].sort().join(', ')}\\n\\n`;\n }\n\n summary += FORK_WAIT_INSTRUCTION;\n\n return summary;\n}\n\nexport async function generateSummaryForFork(\n jsonlPath: string,\n summaryModel?: string,\n includeThinkingInSummary?: boolean,\n summaryHarness: RuntimeName = 'claude-code',\n sourceHarness?: RuntimeName,\n): Promise<{ summary: string; summaryModel: string | null }> {\n if (!summaryModel) {\n // Fork summaries serialize the entire conversation in one shot. Sonnet 4.6's\n // 1M-token context handles large sessions that would overflow Haiku's 200k.\n summaryModel = 'claude-sonnet-4-6';\n }\n\n console.log(`[claude-invoke] purpose=summary-fork | model=${summaryModel} | summaryHarness=${summaryHarness} | sourceHarness=${sourceHarness ?? 'claude-code'} | source=summary-fork.ts:generateSummaryForFork | jsonl=${jsonlPath}`);\n\n const { config } = loadConfigSync();\n const richMode = config.conversations.richCompaction;\n\n // The source harness selects which transcript adapter parses/serializes the\n // JSONL (Pi and Claude Code have different shapes); the summary harness\n // selects which CLI backend runs the summarizer LLM. They are independent —\n // e.g. a Pi source can be summarized by a Claude model.\n const adapter = getTranscriptAdapter(sourceHarness);\n\n try {\n const result = await adapter.compactSummary(jsonlPath, {\n model: summaryModel,\n richMode,\n includeThinking: includeThinkingInSummary,\n harness: summaryHarness,\n });\n console.log(`[claude-invoke] SUCCESS purpose=summary-fork | model=${summaryModel} | outputChars=${result.summary.length}`);\n return { summary: result.summary + FORK_WAIT_INSTRUCTION, summaryModel };\n } catch (err: any) {\n console.error(`[claude-invoke] FAILED purpose=summary-fork | model=${summaryModel} | error=\"${err.message}\"`);\n throw err;\n }\n}async function reserveSummaryForkSessionPromise(\n cwd: string,\n): Promise<{ sessionId: string; sessionFile: string }> {\n // PAN-1862 (NFR-3): delegates to the shared session-fork primitive so the\n // conversation panel and the review convoy fork through one code path.\n const { reserveForkSession } = await import('./session-fork.js');\n return reserveForkSession(cwd);\n}\n\nasync function copySessionFromCompactBoundaryPromise(\n sourcePath: string,\n destPath: string,\n): Promise<void> {\n const { copySessionForFork } = await import('./session-fork.js');\n return copySessionForFork(sourcePath, destPath, { fullHistory: false });\n}\n\nasync function createSummaryForkPromise(\n conv: Conversation,\n options: SummaryForkOptions = {},\n): Promise<SummaryForkResult> {\n const sourceSessionFile = conv.claudeSessionId\n ? sessionFilePath(conv.cwd, conv.claudeSessionId)\n : null;\n if (!sourceSessionFile) {\n throw new Error(`No session file found for conversation ${conv.name}`);\n }\n\n const cwd = options.cwd || conv.cwd || process.cwd();\n const launchModel = options.model || conv.model;\n const summaryModel = options.model || conv.model;\n const forkMode = options.forkMode ?? 'summary';\n console.log(`[summary-fork] Forking conv=${conv.name} launchModel=${launchModel || 'default'} summaryModel=${summaryModel || 'default'} localOnly=${options.localSummaryOnly || false} forkMode=${forkMode}`);\n\n const { sessionId, sessionFile } = await Effect.runPromise(reserveSummaryForkSession(cwd));\n\n let summary: string;\n let usedSummaryModel: string | null;\n let effectiveForkMode = forkMode;\n let handoffDocPath: string | null = null;\n let forkFallbackReason: string | null = null;\n\n if (forkMode === 'plain') {\n // Plain fork: copy raw JSONL from last compact boundary (or full history)\n // into the new session file so Claude Code can --resume it directly.\n await Effect.runPromise(copySessionFromCompactBoundary(sourceSessionFile, sessionFile));\n summary = '';\n usedSummaryModel = null;\n } else if (forkMode === 'handoff') {\n const handoffAuthor: HandoffAuthor = options.handoffAuthor ?? 'external';\n if (handoffAuthor === 'external') {\n // External authoring: read the source JSONL and have a fresh authoring\n // session write the handoff doc. Source conversation is never touched.\n // Works on ended source conversations and lets the user pick the\n // authoring model/harness independently of the source.\n try {\n const handoff = await authorHandoffExternal(\n conv,\n sourceSessionFile,\n options.focus,\n options.handoffAuthorModel,\n options.handoffAuthorHarness,\n );\n summary = handoff.docText;\n usedSummaryModel = options.handoffAuthorModel ?? DEFAULT_HANDOFF_AUTHOR_MODEL;\n handoffDocPath = handoff.docPath;\n } catch (error) {\n forkFallbackReason = handoffFailureReason(error);\n effectiveForkMode = 'summary';\n logHandoffFallback(conv, forkFallbackReason);\n const result = await generateSummarySeed(sourceSessionFile, summaryModel ?? undefined, options.localSummaryOnly, options.includeThinkingInSummary, undefined, conv.harness ?? undefined);\n summary = prependFallbackFocus(result.summary, options.focus, forkFallbackReason);\n usedSummaryModel = result.summaryModel;\n }\n } else {\n // Source authoring (legacy): deliver the handoff prompt to the live\n // source agent and wait for it to write the doc + .done sentinel.\n // Requires the source conversation to be alive and pollutes its\n // context with the handoff turn pair.\n const preconditionFallback = await handoffPreconditionFallbackReason(conv);\n if (preconditionFallback) {\n forkFallbackReason = preconditionFallback;\n effectiveForkMode = 'summary';\n logHandoffFallback(conv, preconditionFallback);\n const result = await generateSummarySeed(sourceSessionFile, summaryModel ?? undefined, options.localSummaryOnly, options.includeThinkingInSummary, undefined, conv.harness ?? undefined);\n summary = prependFallbackFocus(result.summary, options.focus, preconditionFallback);\n usedSummaryModel = result.summaryModel;\n } else {\n try {\n const handoff = await requestHandoffFromAgent(conv, options.focus, {\n timeoutMs: options.handoffTimeoutMs,\n pollIntervalMs: options.handoffPollIntervalMs,\n });\n summary = handoff.docText;\n usedSummaryModel = null;\n handoffDocPath = handoff.docPath;\n } catch (error) {\n forkFallbackReason = handoffFailureReason(error);\n effectiveForkMode = 'summary';\n logHandoffFallback(conv, forkFallbackReason);\n const result = await generateSummarySeed(sourceSessionFile, summaryModel ?? undefined, options.localSummaryOnly, options.includeThinkingInSummary, undefined, conv.harness ?? undefined);\n summary = prependFallbackFocus(result.summary, options.focus, forkFallbackReason);\n usedSummaryModel = result.summaryModel;\n }\n }\n }\n } else {\n const result = await generateSummarySeed(sourceSessionFile, summaryModel ?? undefined, options.localSummaryOnly, options.includeThinkingInSummary, undefined, conv.harness ?? undefined);\n summary = result.summary;\n usedSummaryModel = result.summaryModel;\n }\n\n const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');\n const suffix = randomUUID().slice(0, 4);\n const newName = `${timestamp}-${suffix}`;\n const newTmux = `conv-${newName}`;\n\n let newConv = createConversation({\n name: newName,\n tmuxSession: newTmux,\n cwd,\n issueId: conv.issueId ?? undefined,\n title: effectiveForkMode === 'plain'\n ? `Fork: ${conv.title || conv.name}`\n : effectiveForkMode === 'handoff'\n ? `Handoff: ${conv.title || conv.name}`\n : `Summary Fork: ${conv.title || conv.name}`,\n titleSource: 'manual',\n titleSeed: effectiveForkMode === 'plain'\n ? `Fork of ${conv.name}`\n : effectiveForkMode === 'handoff'\n ? `Handoff of ${conv.name}`\n : `Summary Fork of ${conv.name}`,\n claudeSessionId: sessionId,\n model: launchModel ?? undefined,\n effort: conv.effort ?? undefined,\n harness: options.harness ?? conv.harness ?? undefined,\n });\n if (handoffDocPath) {\n newConv = recordConversationHandoff(conv.name, newConv.name, handoffDocPath);\n }\n if (forkFallbackReason) {\n updateConversationForkFallbackReason(newConv.name, forkFallbackReason);\n newConv = getConversationByName(newConv.name) ?? newConv;\n }\n\n return {\n conversation: newConv,\n sessionId,\n sessionFile,\n summary,\n summaryModel: usedSummaryModel,\n forkMode: effectiveForkMode,\n handoffDocPath,\n forkFallbackReason,\n };\n}\n\n// Re-export runModelSummary for any callers that need it directly\nexport { runModelSummary };\n\n// ─── Effect variants (PAN-1249, additive) ────────────────────────────────────\n//\n// Additive Effect surface for fork helpers. Failures from the underlying\n// fs ops or LLM calls surface as FsError (filesystem failures) or Error\n// (LLM / generation failures). The existing Promise functions remain\n// canonical; these are wrappers for Effect-native callers.\n\n/** Effect variant of generateFallbackSummary. */\nexport function generateFallbackSummary(\n jsonlPath: string,\n): Effect.Effect<string, FsError> {\n return Effect.tryPromise({\n try: () => generateFallbackSummaryPromise(jsonlPath),\n catch: (cause) =>\n new FsError({ path: jsonlPath, operation: 'fallback-summary', cause }),\n });\n}\n\n/** Effect variant of reserveSummaryForkSession. */\nexport function reserveSummaryForkSession(\n cwd: string,\n): Effect.Effect<{ sessionId: string; sessionFile: string }, FsError> {\n return Effect.tryPromise({\n try: () => reserveSummaryForkSessionPromise(cwd),\n catch: (cause) =>\n new FsError({ path: cwd, operation: 'reserve-session', cause }),\n });\n}\n\n/** Effect variant of copySessionFromCompactBoundary. */\nexport function copySessionFromCompactBoundary(\n sourcePath: string,\n destPath: string,\n): Effect.Effect<void, FsError> {\n return Effect.tryPromise({\n try: () => copySessionFromCompactBoundaryPromise(sourcePath, destPath),\n catch: (cause) =>\n new FsError({ path: sourcePath, operation: 'copy-session', cause }),\n });\n}\n\n/** Effect variant of createSummaryFork. */\nexport function createSummaryFork(\n conv: Conversation,\n options: SummaryForkOptions = {},\n): Effect.Effect<SummaryForkResult, FsError> {\n return Effect.tryPromise({\n try: () => createSummaryForkPromise(conv, options),\n catch: (cause) =>\n new FsError({ path: conv.name, operation: 'create-summary-fork', cause }),\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;aA6B8C;wBAMf;;;;;aCjCc;;;;AC0b7C,eAAe,+BAA+B,WAAoC;CAChF,MAAM,EAAE,aAAa,MAAM,OAAO;CAClC,MAAM,SAAS,MAAM,SAAS,WAAW,QAAQ,EAC9C,MAAM,KAAK,CACX,QAAQ,MAAM,EAAE,MAAM,CAAC;CAE1B,MAAM,eAAyB,EAAE;CACjC,MAAM,gCAAgB,IAAI,KAAa;CACvC,MAAM,4BAAY,IAAI,KAAa;AAEnC,MAAK,MAAM,QAAQ,OAAO;EACxB,IAAI;AACJ,MAAI;AACF,WAAQ,KAAK,MAAM,KAAK;UAClB;AACN;;AAGF,MAAI,MAAM,SAAS,UAAU,MAAM,SAAS;GAC1C,MAAM,UAAU,MAAM,QAAQ;AAC9B,OAAI,OAAO,YAAY,YAAY,QAAQ,MAAM;QAC3C,CAAC,QAAQ,MAAM,CAAC,WAAW,iBAAiB,IAAI,CAAC,QAAQ,MAAM,CAAC,WAAW,gBAAgB,CAC7F,cAAa,KAAK,QAAQ,MAAM,CAAC;cAE1B,MAAM,QAAQ,QAAQ;SAC1B,MAAM,SAAS,QAClB,KAAI,MAAM,SAAS,UAAU,MAAM,MAAM,MAAM,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,WAAW,IAAI,CACnF,cAAa,KAAK,MAAM,KAAK,MAAM,CAAC;;;AAM5C,MAAI,MAAM,SAAS,eAAe,MAAM,SAAS,SAAS;GACxD,MAAM,UAAU,MAAM,QAAQ;AAC9B,OAAI,MAAM,QAAQ,QAAQ;SACnB,MAAM,SAAS,QAClB,KAAI,MAAM,SAAS,YAAY;AAC7B,eAAU,IAAI,MAAM,KAAK;AACzB,SAAI,MAAM,SAAS,UAAU,MAAM,SAAS,SAAS;MACnD,MAAM,KAAK,MAAM,OAAO,aAAa,MAAM,OAAO;AAClD,UAAI,GAAI,eAAc,IAAI,GAAG;;;;;;CAQzC,IAAI,UAAU;AACd,YAAW;AAEX,KAAI,aAAa,SAAS,GAAG;AAC3B,aAAW;AACX,OAAK,MAAM,OAAO,aAAa,MAAM,GAAG,GAAG,CACzC,YAAW,KAAK,IAAI,MAAM,GAAG,IAAI,GAAG,IAAI,SAAS,MAAM,QAAQ,GAAG;AAEpE,aAAW;;AAGb,KAAI,cAAc,OAAO,GAAG;AAC1B,aAAW;AACX,OAAK,MAAM,KAAK,CAAC,GAAG,cAAc,CAAC,MAAM,CACvC,YAAW,OAAO,EAAE,QAAQ,kBAAkB,GAAG,CAAC;AAEpD,aAAW;;AAGb,KAAI,UAAU,OAAO,EACnB,YAAW,mBAAmB,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,KAAK,CAAC;AAGjE,YAAW;AAEX,QAAO;;;AAiNT,SAAgB,wBACd,WACgC;AAChC,QAAO,OAAO,WAAW;EACvB,WAAW,+BAA+B,UAAU;EACpD,QAAQ,UACN,IAAI,QAAQ;GAAE,MAAM;GAAW,WAAW;GAAoB;GAAO,CAAC;EACzE,CAAC;;;;qBA5rBkC;aAC6C;mBAChC;cACA;wBACK;0BACO;qBAC+B;cAEvD;oBACgC;AA6CjE,yBAAwB"}
@@ -9,7 +9,7 @@ import { a as resolveGitHubIssueSync, n as init_tracker_utils } from "./tracker-
9
9
  import { A as refreshCacheSync, B as syncStatuslineSync, E as planHooksSyncSync, H as writeSyncManifestSync, I as syncHooksSync, M as removeLegacySkills070Sync, O as planSyncSync, P as syncContextLayersSync, R as syncPiSettingsSync, S as migrateStalePersonalContentSync, V as isStartupSyncNeededSync, f as loadSettingsSync, q as createBackupSync, v as executeSyncSync, w as mirrorProjectSkillsSync } from "./settings-BdvA9r5k.js";
10
10
  import { g as resolveProjectContextFile, u as init_layers } from "./layers-b8Bbomgu.js";
11
11
  import { w as listSessionNames, y as init_tmux } from "./tmux-Ceg1ulra.js";
12
- import { Jt as getAgentStateSync, t as init_agents } from "./agents-CQHK0CSA.js";
12
+ import { Jt as getAgentStateSync, t as init_agents } from "./agents-CgVr77Qq.js";
13
13
  import { h as migrateOverdeckToPanSync, i as init_workspace_manager } from "./workspace-manager-CxoFBq_Y.js";
14
14
  import { i as init_claude_mcp, n as ensurePlaywrightIsolationSync, t as ensureExcalidrawMcpSync } from "./claude-mcp-GkUbJBTY.js";
15
15
  import { Effect } from "effect";
@@ -1154,4 +1154,4 @@ async function syncCommand(options) {
1154
1154
  //#endregion
1155
1155
  export { needsMigrationSync as a, migrateConfigSync as i, cleanupClosedIssueAgentDirectories as n, hasLegacySettingsSync as r, syncCommand as t };
1156
1156
 
1157
- //# sourceMappingURL=sync-BiNVQHBf.js.map
1157
+ //# sourceMappingURL=sync-Bsww9tuj.js.map