@askexenow/exe-os 0.9.340 → 0.9.342

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 (417) hide show
  1. package/README.md +159 -196
  2. package/deploy/compose/.env.customer.example +1 -0
  3. package/deploy/compose/.env.example +1 -0
  4. package/deploy/compose/docker-compose.yml +13 -4
  5. package/deploy/compose/generate-env.ts +4 -0
  6. package/dist/{active-agent-EC6M7KTX.js → active-agent-PVGE57FY.js} +5 -5
  7. package/dist/{active-agent-WHIUCHLA.js → active-agent-TNIO6IY3.js} +5 -5
  8. package/dist/{agentic-ontology-RNSXGXW4.js → agentic-ontology-3FXYMHKW.js} +1 -1
  9. package/dist/{backfill-gate-J7UXFNXU.js → backfill-gate-65LSYVE5.js} +3 -3
  10. package/dist/{backfill-metadata-MFY6BHKE.js → backfill-metadata-ZSMZC45J.js} +8 -8
  11. package/dist/{background-jobs-STURKDCS.js → background-jobs-EXR5RVGG.js} +3 -3
  12. package/dist/backup-verify-3Y2FHGZU.js +425 -0
  13. package/dist/{behaviors-HLZUJSMP.js → behaviors-KYRB3NGJ.js} +9 -9
  14. package/dist/bin/age-ontology-load.js +3 -3
  15. package/dist/bin/agentic-ontology-backfill.js +10 -10
  16. package/dist/bin/agentic-reflection-backfill.js +11 -11
  17. package/dist/bin/agentic-semantic-label.js +10 -10
  18. package/dist/bin/backfill-conversations.js +10 -10
  19. package/dist/bin/backfill-responses.js +10 -10
  20. package/dist/bin/backfill-vectors.js +14 -14
  21. package/dist/bin/bulk-sync-postgres.js +19 -19
  22. package/dist/bin/cc-doctor.js +10 -8
  23. package/dist/bin/cleanup-stale-review-tasks.js +26 -24
  24. package/dist/bin/cli.js +111 -25
  25. package/dist/bin/customer-readiness.js +1 -1
  26. package/dist/bin/deferred-daemon-restart.js +97 -90
  27. package/dist/bin/exe-agent-config.js +9 -8
  28. package/dist/bin/exe-agent.js +20 -20
  29. package/dist/bin/exe-assign.js +12 -12
  30. package/dist/bin/exe-boot.js +201 -68
  31. package/dist/bin/exe-call.js +7 -7
  32. package/dist/bin/exe-cloud.js +20 -20
  33. package/dist/bin/exe-config-dump.js +9 -8
  34. package/dist/bin/exe-dispatch.js +26 -24
  35. package/dist/bin/exe-doctor.js +10 -6
  36. package/dist/bin/exe-export-behaviors.js +11 -11
  37. package/dist/bin/exe-forget.js +12 -10
  38. package/dist/bin/exe-gateway.js +15 -10
  39. package/dist/bin/exe-grant.js +5 -5
  40. package/dist/bin/exe-healthcheck.js +14 -8
  41. package/dist/bin/exe-heartbeat.js +29 -28
  42. package/dist/bin/exe-kill.js +31 -29
  43. package/dist/bin/exe-launch-agent.js +129 -68
  44. package/dist/bin/exe-memory-reads.js +257 -0
  45. package/dist/bin/exe-new-employee.js +12 -11
  46. package/dist/bin/exe-pending-messages.js +27 -25
  47. package/dist/bin/exe-pending-notifications.js +26 -24
  48. package/dist/bin/exe-pending-reviews.js +26 -24
  49. package/dist/bin/exe-rename.js +7 -7
  50. package/dist/bin/exe-review.js +30 -28
  51. package/dist/bin/exe-search.js +11 -9
  52. package/dist/bin/exe-session-cleanup.js +116 -86
  53. package/dist/bin/exe-settings.js +24 -23
  54. package/dist/bin/exe-start-codex.js +163 -41
  55. package/dist/bin/exe-start-opencode.js +15 -15
  56. package/dist/bin/exe-status.js +27 -25
  57. package/dist/bin/exe-support.js +5 -5
  58. package/dist/bin/exe-team.js +5 -5
  59. package/dist/bin/exe-timers.js +32 -213
  60. package/dist/bin/exe-watchdog.js +6 -5
  61. package/dist/bin/git-sweep.js +27 -25
  62. package/dist/bin/graph-backfill.js +9 -9
  63. package/dist/bin/graph-export.js +9 -9
  64. package/dist/bin/import-history.js +11 -11
  65. package/dist/bin/install.js +102 -95
  66. package/dist/bin/intercom-check.js +4 -4
  67. package/dist/bin/mcp-sessions.js +2 -2
  68. package/dist/bin/orchestration-metrics.js +6 -6
  69. package/dist/bin/postgres-agentic-reflection-backfill.js +5 -5
  70. package/dist/bin/postgres-agentic-semantic-backfill.js +7 -7
  71. package/dist/bin/pre-publish.js +5 -3
  72. package/dist/bin/reconcile-backlog.js +484 -0
  73. package/dist/bin/registry-proxy.js +1 -1
  74. package/dist/bin/scan-tasks.js +26 -24
  75. package/dist/bin/setup.js +6 -5
  76. package/dist/bin/shard-migrate.js +9 -9
  77. package/dist/bin/stack-update.js +15 -9
  78. package/dist/bin/update.js +1 -1
  79. package/dist/bin/vps-health-gate.js +1 -1
  80. package/dist/boot-generation-BL2ISK4U.js +22 -0
  81. package/dist/boot-review-sections-DBMQJT5N.js +107 -0
  82. package/dist/branch-hygiene-TZWK6QCF.js +224 -0
  83. package/dist/{branding-JTEOF4ZV.js → branding-DVXBGVDH.js} +3 -3
  84. package/dist/{browser-session-store-CXQMOSEC.js → browser-session-store-B2NR3LIX.js} +2 -1
  85. package/dist/{capability-cards-YOKVMMOL.js → capability-cards-DN5UIKVN.js} +4 -4
  86. package/dist/{capacity-monitor-S54IOWRL.js → capacity-monitor-ZG5GGFR2.js} +27 -25
  87. package/dist/{catchup-brief-DRRQCMKA.js → catchup-brief-D6HH6DWS.js} +32 -29
  88. package/dist/{support-outbox-MGUVVGHT.js → chunk-25X4JD2V.js} +28 -12
  89. package/dist/{chunk-IYB2NIW3.js → chunk-2KZNGDSW.js} +1 -1
  90. package/dist/{chunk-FUIIVSW6.js → chunk-2WAVZ5UJ.js} +1 -1
  91. package/dist/{chunk-K5WFKEXQ.js → chunk-2ZYO73KO.js} +4 -3
  92. package/dist/{chunk-CLAYNAXC.js → chunk-3EUMUSLE.js} +1 -1
  93. package/dist/{chunk-BVV6NHKQ.js → chunk-3SW4OEHB.js} +5 -5
  94. package/dist/{task-enforcement-VI7L43DC.js → chunk-4CMVFVFC.js} +173 -69
  95. package/dist/{chunk-BU2QQWBQ.js → chunk-4J4PMPXZ.js} +3 -2
  96. package/dist/{chunk-JD5S5AAV.js → chunk-4N2NRGCM.js} +2 -2
  97. package/dist/chunk-4QCFFW2H.js +378 -0
  98. package/dist/{chunk-7EA7LYW4.js → chunk-4VSV5P4D.js} +4 -4
  99. package/dist/{chunk-52KNOQMK.js → chunk-4ZCD2UWW.js} +39 -10
  100. package/dist/{chunk-DZ6OQM67.js → chunk-5ERDIOV3.js} +2 -2
  101. package/dist/chunk-5H47VHB7.js +30 -0
  102. package/dist/{chunk-UCQITDLM.js → chunk-5TY4FPH6.js} +1 -1
  103. package/dist/{chunk-R44Z32O2.js → chunk-6CH3TPHR.js} +2 -2
  104. package/dist/{chunk-WYLV4S6A.js → chunk-6WQ3NNCE.js} +3 -3
  105. package/dist/{chunk-HHIBOU3P.js → chunk-6XWTQUO4.js} +1 -1
  106. package/dist/chunk-76CJBM36.js +88 -0
  107. package/dist/{chunk-H2L4PHGH.js → chunk-7CTRGNLW.js} +2 -2
  108. package/dist/{chunk-XUKPALWT.js → chunk-7V4JADD6.js} +2 -2
  109. package/dist/{chunk-RMOBQ6A5.js → chunk-AD2VCHSS.js} +1 -1
  110. package/dist/{chunk-QXSBB26A.js → chunk-AJYCFKJR.js} +1 -1
  111. package/dist/chunk-AM7S2TY6.js +54 -0
  112. package/dist/{chunk-HEPR6QTL.js → chunk-AOHXREB7.js} +334 -66
  113. package/dist/{chunk-FNZR3B7U.js → chunk-ASAFVDVR.js} +1 -1
  114. package/dist/{chunk-TALWBVZE.js → chunk-BUCXNV32.js} +1 -1
  115. package/dist/{chunk-SHIICYJW.js → chunk-BYTDMAGB.js} +4 -2
  116. package/dist/{chunk-32SXNM3G.js → chunk-C5QAPURN.js} +1 -1
  117. package/dist/{chunk-XCTGRD4S.js → chunk-CE7F6RHG.js} +587 -77
  118. package/dist/{chunk-7WPOJDTA.js → chunk-CNVBKDEY.js} +21 -15
  119. package/dist/{chunk-KPIN44GZ.js → chunk-CWXVH7WV.js} +1 -1
  120. package/dist/chunk-D74XYSA3.js +621 -0
  121. package/dist/{chunk-A47MXY2I.js → chunk-EQHQF5IM.js} +50 -32
  122. package/dist/{chunk-AOO77BDE.js → chunk-EUS4QUKP.js} +1 -1
  123. package/dist/{chunk-RLRT2BVM.js → chunk-EV46QTD3.js} +1 -1
  124. package/dist/{chunk-PAEXYFMY.js → chunk-F27XXN6C.js} +1 -1
  125. package/dist/{chunk-XQNNB5GY.js → chunk-F55P3H4S.js} +37 -2
  126. package/dist/{chunk-MWZFF3Q7.js → chunk-F5O7V7UK.js} +1 -1
  127. package/dist/{chunk-LMZISHSJ.js → chunk-FPFPLPUY.js} +6 -6
  128. package/dist/{chunk-7TS64TBG.js → chunk-G4RGKBCA.js} +8 -3
  129. package/dist/{chunk-4IYSBPX3.js → chunk-G5T6KDG4.js} +1 -1
  130. package/dist/{chunk-7WDXEKUL.js → chunk-GBTTBXAF.js} +4 -4
  131. package/dist/{chunk-6MRWJMI5.js → chunk-GDIJE6D7.js} +1 -1
  132. package/dist/{chunk-AVKXCSBI.js → chunk-GG5NDQNN.js} +2 -2
  133. package/dist/{chunk-I3XKO3KQ.js → chunk-GJ26DULE.js} +2 -2
  134. package/dist/chunk-GL57PO3K.js +104 -0
  135. package/dist/chunk-GWDSB25T.js +6 -0
  136. package/dist/{chunk-F55JF43M.js → chunk-H6BV5TKD.js} +39 -24
  137. package/dist/{chunk-HCJCW7X4.js → chunk-H7I47RS5.js} +34 -1
  138. package/dist/{chunk-65SVQELL.js → chunk-HA5R6USD.js} +31 -5
  139. package/dist/{chunk-VJHUVFIW.js → chunk-HAF2DJYV.js} +3 -3
  140. package/dist/chunk-HJ447YER.js +224 -0
  141. package/dist/chunk-HV4KESRY.js +395 -0
  142. package/dist/{chunk-B76TSEP3.js → chunk-I26FWCSP.js} +216 -9
  143. package/dist/{chunk-X2FHTDIU.js → chunk-IBYL4DHM.js} +1 -1
  144. package/dist/{chunk-JCBKARSK.js → chunk-IOEUAQWP.js} +1 -1
  145. package/dist/{chunk-MW2GJJ53.js → chunk-JGIPOUIK.js} +3 -3
  146. package/dist/{chunk-2233TQCA.js → chunk-JLZ4RYSD.js} +2 -2
  147. package/dist/{chunk-Q7L6G6KV.js → chunk-JMYHW3AL.js} +2 -2
  148. package/dist/{chunk-OJTJYEMX.js → chunk-JPUZPAAF.js} +4 -4
  149. package/dist/{chunk-XWHE4LZK.js → chunk-JTBIS4R5.js} +21 -2
  150. package/dist/{chunk-5Z5NATNS.js → chunk-K7KFGDWF.js} +1 -1
  151. package/dist/{chunk-UIV2WPBT.js → chunk-KGWRTRKC.js} +2 -2
  152. package/dist/{chunk-KTPG4XKI.js → chunk-KP3U5QGJ.js} +1 -1
  153. package/dist/{chunk-TLYAEISW.js → chunk-KW2SD2FL.js} +2 -2
  154. package/dist/{chunk-X7H3HMFH.js → chunk-KWZMQACA.js} +11 -7
  155. package/dist/{chunk-OFX5B2S7.js → chunk-L2R3LN2P.js} +1 -1
  156. package/dist/{chunk-T4KZ2Z2N.js → chunk-L4A5DL6Z.js} +3 -3
  157. package/dist/{chunk-X425OEYF.js → chunk-LU2FALL6.js} +3 -3
  158. package/dist/{chunk-6M36JFFD.js → chunk-MKYFAKDW.js} +4 -4
  159. package/dist/{chunk-I5RLNOFK.js → chunk-MOPUCPXK.js} +12 -4
  160. package/dist/{chunk-ZIVEA3NB.js → chunk-MR6LHP4F.js} +1 -1
  161. package/dist/chunk-MVYB76QG.js +341 -0
  162. package/dist/{chunk-2SG2UGCH.js → chunk-MZM6EJCU.js} +4 -4
  163. package/dist/{chunk-ZREGOV2Q.js → chunk-N7QJKQMX.js} +258 -14
  164. package/dist/{chunk-QLFRCA6I.js → chunk-NBZL5CGA.js} +93 -12
  165. package/dist/{chunk-QFPTDSY2.js → chunk-NN2B2QKU.js} +1 -1
  166. package/dist/chunk-NX5INHHG.js +40 -0
  167. package/dist/{chunk-B75BOKEI.js → chunk-NXUSDL37.js} +2 -2
  168. package/dist/{chunk-TPHCZDE3.js → chunk-O5V57XYE.js} +1 -1
  169. package/dist/chunk-O7LWR36Z.js +269 -0
  170. package/dist/{chunk-ATOU6CYR.js → chunk-OLMQ4UOF.js} +57 -11
  171. package/dist/{chunk-6HBURYON.js → chunk-OTF44GXL.js} +4 -4
  172. package/dist/{chunk-DILD54TU.js → chunk-OZEBAGKR.js} +277 -137
  173. package/dist/{chunk-DADMPMYX.js → chunk-P5HIWY2E.js} +1 -1
  174. package/dist/{chunk-EIF2LQHZ.js → chunk-PDWS3E22.js} +55 -4
  175. package/dist/{chunk-KTY7I7UH.js → chunk-PPCDBE6Z.js} +1 -1
  176. package/dist/{chunk-XACL67CS.js → chunk-PQIDGXSG.js} +13 -4
  177. package/dist/chunk-PWUFMX75.js +186 -0
  178. package/dist/{chunk-UPWE5NOG.js → chunk-QDGZJ653.js} +3 -3
  179. package/dist/{chunk-MQRFKSLI.js → chunk-QOO5RW3K.js} +6 -5
  180. package/dist/{chunk-KCC5A7YL.js → chunk-QQ4E2RYW.js} +95 -18
  181. package/dist/{chunk-BPC5LZ6Y.js → chunk-QRYVCDWS.js} +2 -2
  182. package/dist/{chunk-Z42MXYWW.js → chunk-QWVXN224.js} +110 -28
  183. package/dist/{chunk-542EWGY3.js → chunk-QYZJHTNE.js} +26 -27
  184. package/dist/{chunk-I7Z6KTGF.js → chunk-R2NN3JKH.js} +43 -6
  185. package/dist/{chunk-3OBWSCJN.js → chunk-RACAOE2Z.js} +14 -14
  186. package/dist/{chunk-5RACW3LX.js → chunk-RAXVPNMR.js} +1 -1
  187. package/dist/{chunk-2HZY3ECT.js → chunk-RHWZUUS4.js} +6 -6
  188. package/dist/{chunk-RNAYCUNM.js → chunk-S3THSOWV.js} +1 -1
  189. package/dist/{chunk-W7DHFGCB.js → chunk-S7QUM2UK.js} +2 -2
  190. package/dist/{chunk-SSTLTIF3.js → chunk-SKGMF2SO.js} +25 -2
  191. package/dist/{chunk-GN45PBWI.js → chunk-SQKXKV2Y.js} +2 -2
  192. package/dist/{chunk-QZLIZAS7.js → chunk-TLT7NZCW.js} +4 -4
  193. package/dist/chunk-TTCUG2CF.js +91 -0
  194. package/dist/chunk-UEA3LXOB.js +40 -0
  195. package/dist/{chunk-DRCRZIIJ.js → chunk-UNXQBELK.js} +1 -1
  196. package/dist/{chunk-PLRIJALF.js → chunk-UP75Z5X7.js} +921 -176
  197. package/dist/{chunk-TNJRWUCE.js → chunk-USSB56WK.js} +1 -1
  198. package/dist/{chunk-KMBEL7VB.js → chunk-UUEKJDK6.js} +90 -10
  199. package/dist/{chunk-XJI7Z6LZ.js → chunk-UUQBZPJJ.js} +1 -1
  200. package/dist/{chunk-RRWDYOQS.js → chunk-V3EEVY4L.js} +1 -1
  201. package/dist/{chunk-N5I7RL3I.js → chunk-VPZHJCKW.js} +2 -2
  202. package/dist/chunk-VRAQTFD6.js +265 -0
  203. package/dist/{chunk-H37HHX3E.js → chunk-VSRPS3J7.js} +14 -14
  204. package/dist/{chunk-5ZNY7IZI.js → chunk-WMESXBJM.js} +37 -3
  205. package/dist/{chunk-54QCG45Y.js → chunk-WOYADZF5.js} +56 -5
  206. package/dist/{chunk-7Y5UDEVM.js → chunk-WZHZCDDO.js} +6 -5
  207. package/dist/{chunk-3NGZ4PL7.js → chunk-X2Q5NLUL.js} +11 -2
  208. package/dist/{chunk-YE7NH53A.js → chunk-X33NYQYJ.js} +2 -2
  209. package/dist/{chunk-UXBD22CD.js → chunk-XHK3FS3W.js} +3 -2
  210. package/dist/{chunk-3QDSCJ3F.js → chunk-XZXBB3TN.js} +2 -2
  211. package/dist/{chunk-JSKBHUR7.js → chunk-YCANDMI2.js} +2 -2
  212. package/dist/{chunk-JBSCL2XT.js → chunk-YLXTKI6M.js} +1 -1
  213. package/dist/{chunk-G7WO77Z2.js → chunk-YTCFACP4.js} +812 -896
  214. package/dist/{chunk-LTVCRHHD.js → chunk-Z6RSN7KS.js} +7 -6
  215. package/dist/{cleanup-graph-garbage-KUABW6LS.js → cleanup-graph-garbage-XWUFAMFO.js} +6 -6
  216. package/dist/{co-activation-XMPHKGHR.js → co-activation-ZCHPLLP6.js} +4 -4
  217. package/dist/{co-occurrence-SHKSJ6CV.js → co-occurrence-KG3LKALN.js} +4 -4
  218. package/dist/{code-context-index-EXXDY3QZ.js → code-context-index-WSOJQ72R.js} +5 -5
  219. package/dist/{content-extractor-RHITF2KM.js → content-extractor-NXYNEF5K.js} +2 -2
  220. package/dist/{conversation-wiki-populator-WU4HEQ7C.js → conversation-wiki-populator-WQN3IUU6.js} +1 -1
  221. package/dist/{crdt-sync-UJELKSNX.js → crdt-sync-4PFGPZXV.js} +8 -1
  222. package/dist/{crm-webhook-HHNBS7LY.js → crm-webhook-II7Z45Q6.js} +2 -2
  223. package/dist/cto-delegation-gate-JWWOJPNC.js +669 -0
  224. package/dist/{daemon-auth-7IFSV5FG.js → daemon-auth-3FEAHFWC.js} +5 -3
  225. package/dist/{daemon-orchestration-7QB6QOXT.js → daemon-orchestration-FX4XZDR5.js} +46 -27
  226. package/dist/{daemon-ready-5XI5BIJD.js → daemon-ready-4JTSFKAY.js} +1 -1
  227. package/dist/{daemon-ready-signal-E7VMIQ6I.js → daemon-ready-signal-LPAZWQOV.js} +5 -1
  228. package/dist/daemon-restart-circuit-breaker-PEGEIP6H.js +35 -0
  229. package/dist/daemon-timer-report-NM44WQRH.js +25 -0
  230. package/dist/{db-backup-DLD4QZBO.js → db-backup-W2QFSYQC.js} +15 -5
  231. package/dist/{db-restore-events-JVEEP52Z.js → db-restore-events-MQ2DBGEQ.js} +2 -2
  232. package/dist/dispatch-ack-VYH3ONSS.js +27 -0
  233. package/dist/{doc-graph-extractor-3TRLGD7M.js → doc-graph-extractor-F6ODFIID.js} +4 -4
  234. package/dist/{documents-FHIBU7KJ.js → documents-23WX2FPQ.js} +12 -12
  235. package/dist/dreaming-NWD22UQ5.js +46 -0
  236. package/dist/{exe-drift-KY2KHZEL.js → exe-drift-CW72FBGE.js} +5 -5
  237. package/dist/{exe-export-SP6G3QF6.js → exe-export-AUJ7CV7G.js} +11 -11
  238. package/dist/{exe-import-KJU7FBVQ.js → exe-import-JX4I52VE.js} +11 -11
  239. package/dist/{exe-key-M6TYHNUW.js → exe-key-2N6LY2BU.js} +241 -226
  240. package/dist/{exe-org-BGZNNE4H.js → exe-org-X4CSNCKS.js} +3 -3
  241. package/dist/{exe-snapshot-ZNPGUKRZ.js → exe-snapshot-XZ7FVCYZ.js} +31 -29
  242. package/dist/{fast-db-init-QQB6IVWV.js → fast-db-init-4CWHSOOR.js} +1 -1
  243. package/dist/{founder-context-PQR3BUS7.js → founder-context-7L3CFPYN.js} +4 -4
  244. package/dist/gateway/index.js +14 -14
  245. package/dist/{gateway-client-GRBOJQYC.js → gateway-client-7LMZKHTH.js} +1 -1
  246. package/dist/git-main-guard-UZP3KXYI.js +39 -0
  247. package/dist/{git-staleness-UGB5J3K7.js → git-staleness-CI2R4QOJ.js} +4 -4
  248. package/dist/{global-procedures-R7KRREEK.js → global-procedures-XQX3LUH6.js} +6 -6
  249. package/dist/{graph-auto-extract-LKIQ57PL.js → graph-auto-extract-OURWMGBS.js} +4 -4
  250. package/dist/{hook-integrity-WURH6DS3.js → hook-integrity-XY47IM7G.js} +1 -1
  251. package/dist/hook-prefetch-PHRRKY4C.js +137 -0
  252. package/dist/hooks/bug-report-worker.js +32 -28
  253. package/dist/hooks/codex-stop-task-finalizer.js +32 -28
  254. package/dist/hooks/commit-complete.js +29 -27
  255. package/dist/hooks/error-recall.js +12 -11
  256. package/dist/hooks/exe-heartbeat-hook.js +6 -6
  257. package/dist/hooks/ingest-worker.js +11 -11
  258. package/dist/hooks/ingest.js +18 -18
  259. package/dist/hooks/instructions-loaded.js +7 -7
  260. package/dist/hooks/manifest.json +20 -20
  261. package/dist/hooks/notification.js +7 -7
  262. package/dist/hooks/post-compact.js +28 -26
  263. package/dist/hooks/post-tool-combined.js +12 -12
  264. package/dist/hooks/pre-compact.js +29 -27
  265. package/dist/hooks/pre-tool-use.js +180 -47
  266. package/dist/hooks/prompt-submit.js +324 -273
  267. package/dist/hooks/session-end.js +35 -33
  268. package/dist/hooks/session-start.js +28 -21
  269. package/dist/hooks/stop.js +35 -33
  270. package/dist/hooks/subagent-stop.js +28 -26
  271. package/dist/hooks/summary-worker.js +36 -34
  272. package/dist/index.js +39 -37
  273. package/dist/{installer-SB2N5NKH.js → installer-G3JAO3NT.js} +9 -9
  274. package/dist/{installer-MZWWBXM6.js → installer-JND3RJ6Q.js} +9 -9
  275. package/dist/{installer-MNPXDLGG.js → installer-L7EVH5U5.js} +9 -9
  276. package/dist/{intercom-queue-VU2G6F5R.js → intercom-queue-ZFEQALR3.js} +25 -23
  277. package/dist/{key-backup-status-TNEHXA2X.js → key-backup-status-LOSMVATJ.js} +2 -2
  278. package/dist/lib/agent-config.js +4 -3
  279. package/dist/lib/cloud-sync.js +19 -19
  280. package/dist/lib/config.js +2 -2
  281. package/dist/lib/consolidation.js +9 -9
  282. package/dist/lib/crypto.js +3 -3
  283. package/dist/lib/database.js +6 -4
  284. package/dist/lib/db-daemon-client.js +5 -5
  285. package/dist/lib/db-read-worker-client.js +9 -3
  286. package/dist/lib/db.js +6 -4
  287. package/dist/lib/device-registry.js +3 -3
  288. package/dist/lib/embed-worker.js +2 -2
  289. package/dist/lib/embedder.js +5 -5
  290. package/dist/lib/employee-templates.js +7 -7
  291. package/dist/lib/employees.js +10 -4
  292. package/dist/lib/exe-daemon-client.js +4 -4
  293. package/dist/lib/exe-daemon.js +651 -158
  294. package/dist/lib/hybrid-search.js +11 -9
  295. package/dist/lib/identity.js +4 -4
  296. package/dist/lib/keychain.js +1 -1
  297. package/dist/lib/license.js +4 -4
  298. package/dist/lib/messaging.js +28 -24
  299. package/dist/lib/post-tool-memory.js +3 -3
  300. package/dist/lib/registry-proxy.js +1 -1
  301. package/dist/lib/reminders.js +15 -7
  302. package/dist/lib/schedules.js +9 -9
  303. package/dist/lib/session-registry.js +6 -6
  304. package/dist/lib/skill-learning.js +10 -10
  305. package/dist/lib/status-brief.js +4 -4
  306. package/dist/lib/store.js +8 -8
  307. package/dist/lib/task-router.js +5 -5
  308. package/dist/lib/tasks.js +29 -25
  309. package/dist/lib/tmux-routing.js +56 -25
  310. package/dist/lib/tmux-status.js +9 -3
  311. package/dist/lib/tmux-transport.js +1 -1
  312. package/dist/lib/token-spend.js +5 -5
  313. package/dist/lib/transport.js +2 -2
  314. package/dist/lib/ws-client.js +1 -1
  315. package/dist/{license-gate-BYDMFMFT.js → license-gate-QTRQHV4F.js} +5 -5
  316. package/dist/{license-sync-N3RNYVP2.js → license-sync-N3UJFWAW.js} +4 -4
  317. package/dist/mcp/register-tools.js +96 -89
  318. package/dist/mcp/server.js +98 -91
  319. package/dist/mcp/tools/complete-reminder.js +6 -6
  320. package/dist/mcp/tools/create-reminder.js +6 -6
  321. package/dist/mcp/tools/create-task.js +37 -28
  322. package/dist/mcp/tools/deactivate-behavior.js +11 -11
  323. package/dist/mcp/tools/list-reminders.js +6 -6
  324. package/dist/mcp/tools/list-tasks.js +35 -29
  325. package/dist/mcp/tools/send-message.js +28 -26
  326. package/dist/mcp/tools/update-task.js +34 -28
  327. package/dist/{mcp-http-config-EMAVS2JI.js → mcp-http-config-BDL36JZ3.js} +6 -6
  328. package/dist/{mcp-port-config-FFAMMLLE.js → mcp-port-config-6HYY6J7K.js} +2 -2
  329. package/dist/mcp-session-identity-T7H2EKFQ.js +177 -0
  330. package/dist/{memory-cards-TQRLZI4D.js → memory-cards-ZRDW7QBX.js} +8 -8
  331. package/dist/{memory-graph-extractor-IWGNVAFU.js → memory-graph-extractor-QLXDMHRS.js} +5 -5
  332. package/dist/{memory-poisoning-defense-T4ITBGSG.js → memory-poisoning-defense-HJTXYCWH.js} +4 -4
  333. package/dist/{memory-pressure-watchdog-RRIAXPXQ.js → memory-pressure-watchdog-HN5X7AVN.js} +76 -37
  334. package/dist/{memory-queue-X2CSEAKE.js → memory-queue-2UOOF27L.js} +5 -3
  335. package/dist/{memory-queue-client-BKQHTYTH.js → memory-queue-client-O5U6F6XP.js} +6 -6
  336. package/dist/{memory-reflection-6N4J2FL7.js → memory-reflection-WVNFBJAA.js} +4 -4
  337. package/dist/{monitor-grants-RZOVSYVO.js → monitor-grants-E3ELDEWD.js} +5 -5
  338. package/dist/{monitor-identity-MOXISYNG.js → monitor-identity-GLFZSEAH.js} +2 -2
  339. package/dist/notification-ledger-SZEPGJQ7.js +409 -0
  340. package/dist/{notifications-KONB3C62.js → notifications-4UT53CDG.js} +25 -23
  341. package/dist/{orchestration-events-5SIJOE4H.js → orchestration-events-WV4NWKZH.js} +5 -5
  342. package/dist/{orchestration-phase-CXRCBCK7.js → orchestration-phase-AWBXPO52.js} +3 -3
  343. package/dist/orchestrator-UAC3RW2T.js +47 -0
  344. package/dist/{pipeline-router-O6HU7DKH.js → pipeline-router-LM53NMWJ.js} +5 -5
  345. package/dist/{plan-limits-GRNBOBB3.js → plan-limits-2FEY2GOS.js} +7 -7
  346. package/dist/platform-procedures-X5DFJFV7.js +13 -0
  347. package/dist/{preferences-TYTGDXQZ.js → preferences-TWIXZBKS.js} +2 -2
  348. package/dist/{project-boot-SD7GM24K.js → project-boot-4NUH5FRU.js} +2 -2
  349. package/dist/{projection-worker-ZVOFHTXT.js → projection-worker-FZ7YSN3G.js} +5 -5
  350. package/dist/{prospective-memory-W6S67LZZ.js → prospective-memory-6D2ASWM3.js} +4 -4
  351. package/dist/{push-notifications-W5JRXLKJ.js → push-notifications-U6FIBQRW.js} +3 -3
  352. package/dist/{reranker-QATGTAPC.js → reranker-GYYRPANK.js} +3 -3
  353. package/dist/{retrieval-health-TWHKYCRK.js → retrieval-health-4Z242P3B.js} +1 -1
  354. package/dist/{review-polling-PCMD5BFN.js → review-polling-HXAFJZDA.js} +26 -24
  355. package/dist/runtime/index.js +37 -35
  356. package/dist/{session-events-3LCVQEL7.js → session-events-O3BV56ZX.js} +26 -24
  357. package/dist/{session-kill-telemetry-CZSPN5V6.js → session-kill-telemetry-AELE4JDK.js} +26 -24
  358. package/dist/session-mcp-config-YKNXG5DW.js +50 -0
  359. package/dist/{session-scope-CFHVSVAQ.js → session-scope-5EAPUT45.js} +25 -23
  360. package/dist/setup-wizard-NB6R4QVS.js +14 -0
  361. package/dist/{shard-manager-MBWQEYNG.js → shard-manager-35PWFR3B.js} +3 -3
  362. package/dist/{skill-refinement-74T6VDFM.js → skill-refinement-DVSYXBBP.js} +5 -5
  363. package/dist/stack-promote-RSOWO6EY.js +130 -0
  364. package/dist/{stack-release-XXMV7SPX.js → stack-release-HGFFFCGC.js} +112 -22
  365. package/dist/{stack-update-E2TKHHSQ.js → stack-update-ATCJFOEK.js} +7 -5
  366. package/dist/{steward-gate-WT7WXQIE.js → steward-gate-3GQ5XKMJ.js} +5 -5
  367. package/dist/support-inbox-4L62P4EP.js +33 -0
  368. package/dist/support-outbox-WHGPUYL2.js +16 -0
  369. package/dist/task-enforcement-IPIZX2MM.js +90 -0
  370. package/dist/{task-scope-CQEG5SSJ.js → task-scope-FOIW25HR.js} +27 -23
  371. package/dist/{tasks-crud-UGK5A25L.js → tasks-crud-HBDKE6C5.js} +35 -23
  372. package/dist/tasks-notify-G2U2U2YA.js +53 -0
  373. package/dist/{tasks-review-JKBRACII.js → tasks-review-27SFX5XN.js} +27 -23
  374. package/dist/{telemetry-upload-7LBYGPCB.js → telemetry-upload-ZOXHRWR5.js} +9 -9
  375. package/dist/tmux-command-guard-KSJKIX3C.js +117 -0
  376. package/dist/{token-budget-QBRJR4M5.js → token-budget-5Y4YPDO3.js} +5 -5
  377. package/dist/{tool-capability-index-Q6YQDL7I.js → tool-capability-index-VYF5GNJ2.js} +1 -1
  378. package/dist/{tool-telemetry-ULIDUK3P.js → tool-telemetry-KBXSBDTK.js} +1 -1
  379. package/dist/tui/App.js +40 -38
  380. package/dist/{tui-data-NPTGKT37.js → tui-data-NLFMBIQI.js} +25 -23
  381. package/dist/{update-check-HFO4VHO4.js → update-check-3FHAWIG2.js} +7 -3
  382. package/dist/update-support-UVUSZG7R.js +355 -0
  383. package/dist/{whatsapp-accounts-BWRZUY23.js → whatsapp-accounts-BFIOXYW5.js} +1 -1
  384. package/dist/{whatsapp-config-GOSELKTE.js → whatsapp-config-FAX6EA5K.js} +20 -13
  385. package/dist/{worker-gate-MXUQUG4M.js → worker-gate-3KUBAU32.js} +3 -3
  386. package/dist/{workflow-engine-EWCOLD47.js → workflow-engine-YITHXZR5.js} +2 -2
  387. package/dist/{working-memory-PY4AG577.js → working-memory-NUVB5SFV.js} +1 -1
  388. package/dist/worktree-LL62PLQB.js +47 -0
  389. package/dist/{worktree-sweep-ZAG3V24E.js → worktree-sweep-WTK3MTKH.js} +7 -7
  390. package/package.json +3 -2
  391. package/release-notes.json +86 -76
  392. package/dist/branch-hygiene-R5NVTRAY.js +0 -138
  393. package/dist/chunk-6IRVGOUL.js +0 -30
  394. package/dist/chunk-HMP5VT6A.js +0 -118
  395. package/dist/chunk-PVYR4FCQ.js +0 -113
  396. package/dist/chunk-UFAXZ2PY.js +0 -138
  397. package/dist/chunk-Y25OJWOQ.js +0 -45
  398. package/dist/chunk-YGAAZN3E.js +0 -47
  399. package/dist/cto-delegation-gate-DXNF5HVW.js +0 -290
  400. package/dist/dispatch-ack-4S44HBWB.js +0 -15
  401. package/dist/dreaming-OMFUEUCQ.js +0 -44
  402. package/dist/orchestrator-DUUX7FQV.js +0 -45
  403. package/dist/setup-wizard-TOFEJPLF.js +0 -13
  404. package/dist/tasks-notify-SMLDFBWM.js +0 -50
  405. package/dist/worktree-GVF2I5FP.js +0 -31
  406. /package/dist/{chunk-FLNSJ2TC.js → chunk-GIBYNT6W.js} +0 -0
  407. /package/dist/{chunk-NQBRGXYR.js → chunk-JRM4TAGS.js} +0 -0
  408. /package/dist/{chunk-WHRTDN5J.js → chunk-MGGOSFCR.js} +0 -0
  409. /package/dist/{chunk-D3QASX2W.js → chunk-QIFYLIUF.js} +0 -0
  410. /package/dist/{chunk-OADURIIN.js → chunk-WTH4CSAP.js} +0 -0
  411. /package/dist/{core-memory-VO5ZIBKH.js → core-memory-GAC6EWOQ.js} +0 -0
  412. /package/dist/{entity-boost-DNI4A5I7.js → entity-boost-NT6UO2Z3.js} +0 -0
  413. /package/dist/{message-queue-client-Q5BW3O5W.js → message-queue-client-4EOQRUXW.js} +0 -0
  414. /package/dist/{oauth-server-ROQFOEMM.js → oauth-server-67YBDP32.js} +0 -0
  415. /package/dist/{webhook-pipe-FZYLO6RR.js → webhook-pipe-LEXMUQ5B.js} +0 -0
  416. /package/dist/{wiki-acl-JA5UCQZ4.js → wiki-acl-74BLWG6J.js} +0 -0
  417. /package/dist/{wiki-client-VNDOBK63.js → wiki-client-EAN7F5LV.js} +0 -0
@@ -1,14 +1,21 @@
1
1
  import {
2
2
  ensureAgentSymlink
3
- } from "./chunk-OFX5B2S7.js";
3
+ } from "./chunk-L2R3LN2P.js";
4
4
  import {
5
5
  ensureWorktree,
6
+ ensureWorktreeAsync,
6
7
  getGitRoot,
8
+ isInsideGitRepo,
7
9
  isWorktreeDirty
8
- } from "./chunk-B76TSEP3.js";
10
+ } from "./chunk-I26FWCSP.js";
9
11
  import {
10
12
  incrementSkillSuccess
11
- } from "./chunk-RLRT2BVM.js";
13
+ } from "./chunk-EV46QTD3.js";
14
+ import {
15
+ LEASE_MINUTES,
16
+ isLeaseExpired,
17
+ leaseExpiryIso
18
+ } from "./chunk-E23LNUWJ.js";
12
19
  import {
13
20
  buildTaskResult,
14
21
  serializeMessage
@@ -17,42 +24,38 @@ import {
17
24
  reviewSignalPath,
18
25
  taskSignalDir
19
26
  } from "./chunk-EIKATSE4.js";
20
- import {
21
- LEASE_MINUTES,
22
- leaseExpiryIso
23
- } from "./chunk-E23LNUWJ.js";
24
27
  import {
25
28
  listSessions,
26
29
  registerSession
27
- } from "./chunk-UIV2WPBT.js";
28
- import {
29
- listTmuxSessions
30
- } from "./chunk-SSTLTIF3.js";
30
+ } from "./chunk-KGWRTRKC.js";
31
31
  import {
32
32
  getTransport
33
- } from "./chunk-RMOBQ6A5.js";
33
+ } from "./chunk-AD2VCHSS.js";
34
+ import {
35
+ listTmuxSessions
36
+ } from "./chunk-SKGMF2SO.js";
34
37
  import {
35
38
  recordOrchestrationEventBestEffort
36
- } from "./chunk-FUIIVSW6.js";
39
+ } from "./chunk-2WAVZ5UJ.js";
37
40
  import {
38
41
  assertRuntimeModelCompatible,
39
42
  getAgentRuntime,
40
43
  normalizeCcModelName,
41
44
  resolveSpawnRuntimeConfig
42
- } from "./chunk-65SVQELL.js";
43
- import {
44
- RUNTIME_TABLE
45
- } from "./chunk-NGP6LSV2.js";
45
+ } from "./chunk-HA5R6USD.js";
46
46
  import {
47
47
  PlanLimitError,
48
48
  assertEmployeeLimitSync
49
- } from "./chunk-MW2GJJ53.js";
49
+ } from "./chunk-JGIPOUIK.js";
50
+ import {
51
+ BUSY_PATTERN
52
+ } from "./chunk-GWDSB25T.js";
50
53
  import {
51
54
  getSessionKey
52
55
  } from "./chunk-CVYC6DUW.js";
53
56
  import {
54
57
  writeMemoryViaDaemon
55
- } from "./chunk-2233TQCA.js";
58
+ } from "./chunk-JLZ4RYSD.js";
56
59
  import {
57
60
  getProjectName
58
61
  } from "./chunk-OPU3NYOO.js";
@@ -72,23 +75,27 @@ import {
72
75
  getEmployee,
73
76
  isCoordinatorName,
74
77
  loadEmployees,
78
+ loadEmployeesState,
75
79
  loadEmployeesSync,
76
80
  offloadReadIfMain
77
- } from "./chunk-HEPR6QTL.js";
81
+ } from "./chunk-AOHXREB7.js";
82
+ import {
83
+ RUNTIME_TABLE
84
+ } from "./chunk-NGP6LSV2.js";
78
85
  import {
79
86
  loadDeviceId
80
- } from "./chunk-XACL67CS.js";
87
+ } from "./chunk-PQIDGXSG.js";
81
88
  import {
82
89
  EXE_AI_DIR
83
- } from "./chunk-7Y5UDEVM.js";
90
+ } from "./chunk-WZHZCDDO.js";
84
91
  import {
85
92
  atomicWriteJsonSync,
86
93
  atomicWriteSync
87
- } from "./chunk-SHIICYJW.js";
94
+ } from "./chunk-BYTDMAGB.js";
88
95
 
89
96
  // src/lib/tmux-routing.ts
90
- import { execFile, execFileSync as execFileSync2 } from "child_process";
91
- import { promisify } from "util";
97
+ import { execFile as execFile2, execFileSync as execFileSync2 } from "child_process";
98
+ import { promisify as promisify3 } from "util";
92
99
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, existsSync as existsSync9, appendFileSync as appendFileSync3, openSync as openSync2, closeSync as closeSync2, writeSync as writeSync2, renameSync as renameSync3, constants as constants2, statSync as statSync4 } from "fs";
93
100
  import path8 from "path";
94
101
  import os6 from "os";
@@ -412,6 +419,7 @@ var QUEUE_PATH = path3.join(os3.homedir(), ".exe-os", "intercom-queue.json");
412
419
  var QUEUE_LOCK_PATH = `${QUEUE_PATH}.lock`;
413
420
  var MAX_RETRIES = 10;
414
421
  var TTL_MS = 60 * 60 * 1e3;
422
+ var HARD_TTL_MS = 2 * TTL_MS;
415
423
  function ensureDir() {
416
424
  const dir = path3.dirname(QUEUE_PATH);
417
425
  if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
@@ -555,7 +563,7 @@ function drainQueue(isSessionBusy2, sendKeys, notifyExpiry = notifyExpiredInterc
555
563
  }
556
564
  return { drained, failed };
557
565
  }
558
- async function drainQueueAsync(isSessionBusy2, sendKeys, notifyExpiry = notifyExpiredIntercom, yieldEvery = 10) {
566
+ async function drainQueueAsync(isSessionBusy2, sendKeys, notifyExpiry = notifyExpiredIntercom, yieldEvery = 10, disposition = async () => "declined") {
559
567
  if (_draining) {
560
568
  logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
561
569
  return { drained: 0, failed: 0 };
@@ -579,7 +587,25 @@ async function drainQueueAsync(isSessionBusy2, sendKeys, notifyExpiry = notifyEx
579
587
  processed++;
580
588
  const age = Date.now() - new Date(item.queuedAt).getTime();
581
589
  if (age > TTL_MS) {
582
- logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
590
+ let outcome = "declined";
591
+ try {
592
+ outcome = await disposition(item);
593
+ } catch (err) {
594
+ logQueue(`DISPOSITION_FAILED \u2192 ${item.targetSession}: ${err instanceof Error ? err.message : String(err)}`);
595
+ outcome = "error";
596
+ }
597
+ if (outcome === "dispositioned") {
598
+ logQueue(`DISPOSITIONED (expired) \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old \u2192 task routed to blocked, reason: ${item.reason})`);
599
+ failed++;
600
+ continue;
601
+ }
602
+ if (outcome === "error" && age <= HARD_TTL_MS) {
603
+ logQueue(`TTL_DEFERRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, disposition error \u2014 retrying; hard cap ${Math.round(HARD_TTL_MS / 6e4)}min)`);
604
+ remaining.push(item);
605
+ failed++;
606
+ continue;
607
+ }
608
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason}${outcome === "error" ? "; disposition erroring past hard cap" : ""})`);
583
609
  try {
584
610
  notifyExpiry(item);
585
611
  } catch (err) {
@@ -601,7 +627,19 @@ async function drainQueueAsync(isSessionBusy2, sendKeys, notifyExpiry = notifyEx
601
627
  }
602
628
  item.attempts++;
603
629
  if (item.attempts >= MAX_RETRIES) {
604
- logQueue(`PENDING \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, keeping queued, reason: ${item.reason})`);
630
+ let outcome = "declined";
631
+ try {
632
+ outcome = await disposition(item);
633
+ } catch (err) {
634
+ logQueue(`DISPOSITION_FAILED \u2192 ${item.targetSession}: ${err instanceof Error ? err.message : String(err)}`);
635
+ outcome = "error";
636
+ }
637
+ if (outcome === "dispositioned") {
638
+ logQueue(`DISPOSITIONED \u2192 ${item.targetSession} (${MAX_RETRIES} attempts exhausted \u2192 task routed to blocked, reason: ${item.reason})`);
639
+ failed++;
640
+ continue;
641
+ }
642
+ logQueue(`PENDING \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, keeping queued, reason: ${item.reason}${outcome === "error" ? "; disposition error" : ""})`);
605
643
  item.attempts = MAX_RETRIES;
606
644
  remaining.push(item);
607
645
  failed++;
@@ -800,7 +838,8 @@ import { homedir as homedir2 } from "os";
800
838
  import crypto2 from "crypto";
801
839
  import path5 from "path";
802
840
  import os5 from "os";
803
- import { execSync as execSync2 } from "child_process";
841
+ import { execSync as execSync2, execFile as execFileCb } from "child_process";
842
+ import { promisify } from "util";
804
843
  import { mkdir, writeFile, appendFile } from "fs/promises";
805
844
  import { existsSync as existsSync6, readFileSync as readFileSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
806
845
 
@@ -890,6 +929,7 @@ function clearStaleReviewEscalation(taskId) {
890
929
  }
891
930
 
892
931
  // src/lib/tasks-crud.ts
932
+ var execFileAsync = promisify(execFileCb);
893
933
  var DEFAULT_TASK_PRIORITY = "p1";
894
934
  function normalizeTaskPriority(value) {
895
935
  const v = typeof value === "string" ? value.trim().toLowerCase() : "";
@@ -1293,7 +1333,7 @@ async function createTaskCore(input) {
1293
1333
  if (isCoordinatorSession) {
1294
1334
  earlySessionScope = resolved;
1295
1335
  } else {
1296
- const { getSessionProject } = await import("./session-scope-CFHVSVAQ.js");
1336
+ const { getSessionProject } = await import("./session-scope-5EAPUT45.js");
1297
1337
  const sessionProject = getSessionProject(resolved);
1298
1338
  if (sessionProject && sessionProject !== input.projectName) {
1299
1339
  scopeMismatchWarning = `session/project mismatch: session "${resolved}" owns "${sessionProject}" but task targets "${input.projectName}". Routed to default scope.`;
@@ -1374,10 +1414,26 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
1374
1414
  const complexity = input.complexity ?? "standard";
1375
1415
  const priority = normalizeTaskPriority(input.priority);
1376
1416
  const sessionScope = earlySessionScope;
1417
+ const idempotencyKey = input.idempotencyKey ? input.idempotencyKey : input.idempotencyWindow ? crypto2.createHash("sha256").update(
1418
+ // PREIMAGE MUST BE INJECTIVE. A NUL-joined concatenation is NOT: nothing
1419
+ // rejects a NUL inside these fields (they arrive as plain z.string()), so two
1420
+ // DIFFERENT tuples can produce the SAME joined string and therefore the same
1421
+ // key — silently collapsing two distinct tasks into one. JSON.stringify of the
1422
+ // array is injective: each field is quoted, escaped and delimited by the encoding.
1423
+ JSON.stringify([
1424
+ input.assignedTo,
1425
+ input.title,
1426
+ input.projectName,
1427
+ sessionScope ?? "",
1428
+ input.idempotencyWindow
1429
+ ])
1430
+ ).digest("hex") : null;
1377
1431
  try {
1378
- await client.execute({
1379
- sql: `INSERT OR IGNORE INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, spawn_runtime, spawn_model, device_id, founder_id, created_at, updated_at)
1380
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1432
+ const res = await client.execute({
1433
+ sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, spawn_runtime, spawn_model, device_id, founder_id, idempotency_key, created_at, updated_at)
1434
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1435
+ ON CONFLICT(idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
1436
+ RETURNING id`,
1381
1437
  args: [
1382
1438
  id,
1383
1439
  input.title,
@@ -1401,10 +1457,43 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
1401
1457
  input.spawnModel ?? null,
1402
1458
  loadDeviceId(),
1403
1459
  input.founderId ?? null,
1460
+ idempotencyKey,
1404
1461
  now,
1405
1462
  now
1406
1463
  ]
1407
1464
  });
1465
+ if (res.rows.length === 0 && idempotencyKey) {
1466
+ const existing = await client.execute({
1467
+ sql: "SELECT * FROM tasks WHERE idempotency_key = ? LIMIT 1",
1468
+ args: [idempotencyKey]
1469
+ });
1470
+ if (existing.rows.length > 0) {
1471
+ const r = existing.rows[0];
1472
+ return {
1473
+ id: String(r.id),
1474
+ title: String(r.title),
1475
+ assignedTo: String(r.assigned_to),
1476
+ assignedBy: String(r.assigned_by),
1477
+ reviewer: r.reviewer !== null && r.reviewer !== void 0 ? String(r.reviewer) : null,
1478
+ projectName: String(r.project_name),
1479
+ priority: String(r.priority),
1480
+ status: String(r.status),
1481
+ taskFile: String(r.task_file),
1482
+ createdAt: String(r.created_at),
1483
+ updatedAt: String(r.updated_at),
1484
+ checkpointCount: Number(r.checkpoint_count ?? 0),
1485
+ warning: "idempotent no-op: existing task returned for reused idempotency key",
1486
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
1487
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
1488
+ tokensUsed: Number(r.tokens_used ?? 0),
1489
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null,
1490
+ spawnRuntime: r.spawn_runtime !== null && r.spawn_runtime !== void 0 ? String(r.spawn_runtime) : null,
1491
+ spawnModel: r.spawn_model !== null && r.spawn_model !== void 0 ? String(r.spawn_model) : null,
1492
+ sessionScope: r.session_scope !== null && r.session_scope !== void 0 ? String(r.session_scope) : null,
1493
+ deduplicated: true
1494
+ };
1495
+ }
1496
+ }
1408
1497
  } catch (dbErr) {
1409
1498
  process.stderr.write(
1410
1499
  `[create-task] DB insert failed for "${input.title}": ${dbErr instanceof Error ? dbErr.message : String(dbErr)}
@@ -1491,8 +1580,7 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
1491
1580
  sessionScope
1492
1581
  };
1493
1582
  }
1494
- async function queryTaskRows(opts) {
1495
- const client = getClient();
1583
+ async function buildTaskRowsStatement(opts) {
1496
1584
  const conditions = [];
1497
1585
  const args = [];
1498
1586
  if (opts.assignedTo) {
@@ -1542,7 +1630,7 @@ async function queryTaskRows(opts) {
1542
1630
  }
1543
1631
  if (opts.founderId !== null) {
1544
1632
  try {
1545
- const { founderScopeFilter } = await import("./founder-context-PQR3BUS7.js");
1633
+ const { founderScopeFilter } = await import("./founder-context-7L3CFPYN.js");
1546
1634
  const founderFilter = founderScopeFilter(opts.founderId);
1547
1635
  const cleaned = founderFilter.sql.replace(/^\s*AND\s+/, "");
1548
1636
  conditions.push(cleaned);
@@ -1560,12 +1648,15 @@ async function queryTaskRows(opts) {
1560
1648
  const cols = opts.columns ?? "*";
1561
1649
  const order = opts.orderBy ?? "priority ASC, created_at DESC";
1562
1650
  const limit = opts.limit ?? 100;
1563
- const result = await offloadReadIfMain(
1564
- client,
1565
- `SELECT ${cols} FROM tasks ${where} ORDER BY ${order} LIMIT ?`,
1566
- [...args, limit],
1567
- true
1568
- );
1651
+ return {
1652
+ sql: `SELECT ${cols} FROM tasks ${where} ORDER BY ${order} LIMIT ?`,
1653
+ args: [...args, limit]
1654
+ };
1655
+ }
1656
+ async function queryTaskRows(opts) {
1657
+ const client = getClient();
1658
+ const stmt = await buildTaskRowsStatement(opts);
1659
+ const result = await offloadReadIfMain(client, stmt.sql, stmt.args, true);
1569
1660
  return result.rows;
1570
1661
  }
1571
1662
  function getProjectNameSafe() {
@@ -1584,12 +1675,29 @@ async function listTasks(input) {
1584
1675
  }
1585
1676
  const callerSession = input.callerSession?.trim();
1586
1677
  if (input.isOwnQuery && callerSession) {
1587
- extraConditions.push("(instance_id IS NULL OR instance_id = ?)");
1588
- extraArgs.push(callerSession);
1589
- extraConditions.push(
1590
- "(assigned_tmux IS NULL OR assigned_tmux = 'unknown' OR assigned_tmux = ? OR assigned_tmux LIKE ?)"
1591
- );
1592
- extraArgs.push(callerSession, `${callerSession}:%`);
1678
+ const live = await resolveLiveTmuxSessions();
1679
+ const liveForeign = live.verified ? [...live.sessions].filter((s) => s !== callerSession) : null;
1680
+ if (liveForeign === null) {
1681
+ extraConditions.push("(instance_id IS NULL OR instance_id = ?)");
1682
+ extraArgs.push(callerSession);
1683
+ extraConditions.push(
1684
+ "(assigned_tmux IS NULL OR assigned_tmux = 'unknown' OR assigned_tmux = ? OR assigned_tmux LIKE ?)"
1685
+ );
1686
+ extraArgs.push(callerSession, `${callerSession}:%`);
1687
+ } else if (liveForeign.length === 0) {
1688
+ } else {
1689
+ const ph = liveForeign.map(() => "?").join(", ");
1690
+ extraConditions.push(
1691
+ `(instance_id IS NULL OR instance_id = ? OR instance_id NOT IN (${ph}))`
1692
+ );
1693
+ extraArgs.push(callerSession, ...liveForeign);
1694
+ const notLive = liveForeign.map(() => "(assigned_tmux <> ? AND assigned_tmux NOT LIKE ?)").join(" AND ");
1695
+ extraConditions.push(
1696
+ `(assigned_tmux IS NULL OR assigned_tmux = 'unknown' OR assigned_tmux = ? OR assigned_tmux LIKE ? OR (${notLive}))`
1697
+ );
1698
+ extraArgs.push(callerSession, `${callerSession}:%`);
1699
+ for (const s of liveForeign) extraArgs.push(s, `${s}:%`);
1700
+ }
1593
1701
  }
1594
1702
  const callerRoot = callerSession ? callerSession.includes("-") ? callerSession.split("-").pop() || null : callerSession : null;
1595
1703
  const queryOpts = {
@@ -1618,9 +1726,11 @@ async function listTasks(input) {
1618
1726
  sessionScope: input.crossSession ? null : callerRoot ?? void 0,
1619
1727
  columns: "*",
1620
1728
  limit: 1e3,
1621
- orderBy: input.status ? "priority ASC, created_at DESC" : "CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC",
1622
- // When no status filter, listTasks defaults to active + blocked
1623
- ...input.status ? {} : { status: ["open", "in_progress", "blocked"] },
1729
+ orderBy: input.status ? "priority ASC, created_at DESC" : "CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 WHEN 'needs_review' THEN 3 ELSE 4 END, priority ASC, created_at DESC",
1730
+ // When no status filter, listTasks defaults to active + blocked + review queue.
1731
+ // needs_review MUST be included or a coordinator's default list hides the
1732
+ // review queue (bug 50ce5079: "No tasks found" while reviews stall).
1733
+ ...input.status ? {} : { status: ["open", "in_progress", "blocked", "needs_review"] },
1624
1734
  ...extraConditions.length > 0 ? { extraConditions, extraArgs } : {}
1625
1735
  };
1626
1736
  const rows = await queryTaskRows(queryOpts);
@@ -1655,6 +1765,63 @@ function tmuxCmd(args) {
1655
1765
  const sock = process.env.EXE_TMUX_SOCKET;
1656
1766
  return sock ? `tmux -L ${sock} ${args}` : `tmux ${args}`;
1657
1767
  }
1768
+ var _liveSessionsCacheTs = 0;
1769
+ var _liveSessionsCache = { sessions: /* @__PURE__ */ new Set(), verified: false };
1770
+ var _liveSessionsInflight = null;
1771
+ var _liveSessionsCacheSocket = void 0;
1772
+ var _liveSessionsTmuxCalls = 0;
1773
+ function __getLiveSessionsTmuxCalls() {
1774
+ return _liveSessionsTmuxCalls;
1775
+ }
1776
+ function __resetLiveSessionsCache() {
1777
+ _liveSessionsCacheTs = 0;
1778
+ _liveSessionsCache = { sessions: /* @__PURE__ */ new Set(), verified: false };
1779
+ _liveSessionsInflight = null;
1780
+ _liveSessionsTmuxCalls = 0;
1781
+ }
1782
+ async function tmuxListLiveSessions() {
1783
+ _liveSessionsTmuxCalls += 1;
1784
+ const sock = process.env.EXE_TMUX_SOCKET;
1785
+ const args = [
1786
+ ...sock ? ["-L", sock] : [],
1787
+ "list-panes",
1788
+ "-a",
1789
+ "-F",
1790
+ "#{session_name} #{pane_dead}"
1791
+ ];
1792
+ try {
1793
+ const { stdout } = await execFileAsync("tmux", args, { timeout: 2e3, encoding: "utf8" });
1794
+ const sessions = /* @__PURE__ */ new Set();
1795
+ let verified = false;
1796
+ for (const line of stdout.split("\n")) {
1797
+ const [session, paneDead] = line.split(" ");
1798
+ if (!session) continue;
1799
+ verified = true;
1800
+ if (paneDead !== "1") sessions.add(session);
1801
+ }
1802
+ return { sessions, verified };
1803
+ } catch {
1804
+ return { sessions: /* @__PURE__ */ new Set(), verified: false };
1805
+ }
1806
+ }
1807
+ async function resolveLiveTmuxSessions() {
1808
+ const now = Date.now();
1809
+ const sock = process.env.EXE_TMUX_SOCKET;
1810
+ const rawTtl = process.env.EXE_TMUX_ALIVE_CACHE_TTL_MS;
1811
+ const ttl = rawTtl !== void 0 && rawTtl !== "" ? Number(rawTtl) : 2e3;
1812
+ const fresh = now - _liveSessionsCacheTs < ttl;
1813
+ if (fresh && sock === _liveSessionsCacheSocket) return _liveSessionsCache;
1814
+ if (_liveSessionsInflight) return _liveSessionsInflight;
1815
+ _liveSessionsInflight = tmuxListLiveSessions().then((result) => {
1816
+ _liveSessionsCache = result;
1817
+ _liveSessionsCacheTs = Date.now();
1818
+ _liveSessionsCacheSocket = sock;
1819
+ return result;
1820
+ }).finally(() => {
1821
+ _liveSessionsInflight = null;
1822
+ });
1823
+ return _liveSessionsInflight;
1824
+ }
1658
1825
  function refreshTmuxAliveCacheSync() {
1659
1826
  const now = Date.now();
1660
1827
  if (now - _tmuxAliveCacheTs < TMUX_ALIVE_CACHE_TTL_MS) return;
@@ -1682,13 +1849,40 @@ function refreshTmuxAliveCacheSync() {
1682
1849
  }
1683
1850
  function isTmuxSessionAlive(identifier) {
1684
1851
  if (!identifier || identifier === "unknown") return true;
1852
+ const sessionIdentifier = identifier.startsWith("%") ? identifier : identifier.split(":")[0] ?? identifier;
1685
1853
  refreshTmuxAliveCacheSync();
1686
1854
  if (!_tmuxAliveCacheVerified) return true;
1687
- if (identifier.startsWith("%")) return _tmuxAlivePanes.has(identifier);
1688
- return _tmuxAliveSessions.has(identifier);
1855
+ if (sessionIdentifier.startsWith("%")) return _tmuxAlivePanes.has(sessionIdentifier);
1856
+ return _tmuxAliveSessions.has(sessionIdentifier);
1689
1857
  }
1690
1858
  var DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
1691
1859
  var TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
1860
+ var TASK_SIDECHAIN_WRITE_BLOCKED_PREFIX = "TASK_SIDECHAIN_WRITE_BLOCKED";
1861
+ var OWNER_RECLAIM_MAX_CHURN = 5;
1862
+ var OWNER_RECLAIM_CHURN_WINDOW_MS = 1e4;
1863
+ var _ownerReclaimChurn = /* @__PURE__ */ new Map();
1864
+ function instanceToken(session, employees) {
1865
+ const s = (session ?? "").trim();
1866
+ if (!s || s === "unknown" || s.startsWith("%")) return null;
1867
+ const name = (s.split(":")[0] ?? s).trim();
1868
+ if (!name) return null;
1869
+ const lower = name.toLowerCase();
1870
+ const roster = employees ?? loadEmployeesSync();
1871
+ const sorted = [...roster].sort((a, b) => b.name.length - a.name.length);
1872
+ for (const emp of sorted) {
1873
+ const empName = emp.name.toLowerCase();
1874
+ if (lower === empName) return empName;
1875
+ if (lower.startsWith(empName)) {
1876
+ const rest = lower.slice(empName.length);
1877
+ const m = rest.match(/^(\d*)(?:-|$)/);
1878
+ if (m) return empName + m[1];
1879
+ }
1880
+ }
1881
+ const lastDash = name.lastIndexOf("-");
1882
+ const token = (lastDash > 0 ? name.slice(0, lastDash) : name).toLowerCase();
1883
+ if (token.includes("-")) return null;
1884
+ return token || null;
1885
+ }
1692
1886
  var VALID_TASK_STATUSES = [
1693
1887
  "open",
1694
1888
  "in_progress",
@@ -1737,6 +1931,11 @@ async function updateTaskStatus(input) {
1737
1931
  const row = await resolveTask(client, input.taskId);
1738
1932
  const taskId = String(row.id);
1739
1933
  const taskFile = String(row.task_file);
1934
+ if (input.callerIsSidechain && !String(row.assigned_to ?? "").startsWith("subagent:")) {
1935
+ throw new Error(
1936
+ `${TASK_SIDECHAIN_WRITE_BLOCKED_PREFIX}: task ${taskId} belongs to the parent session; a sub-agent/sidechain may not change its status. Report findings back to the parent \u2014 the owning employee updates the task.`
1937
+ );
1938
+ }
1740
1939
  const requestedStatus = input.status;
1741
1940
  const eventSessionScope = row.session_scope ? String(row.session_scope) : null;
1742
1941
  const eventProjectName = row.project_name ? String(row.project_name) : null;
@@ -1850,25 +2049,30 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1850
2049
  });
1851
2050
  if (claim2.rowsAffected === 0) {
1852
2051
  const current = await client.execute({
1853
- sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
2052
+ sql: "SELECT status, assigned_tmux, assigned_by, lease_expires_at FROM tasks WHERE id = ?",
1854
2053
  args: [taskId]
1855
2054
  });
1856
2055
  const cur = current.rows[0];
1857
2056
  const curStatus = cur?.status ?? "unknown";
1858
2057
  const claimedBySession = cur?.assigned_tmux ?? "";
1859
2058
  const assignedBy = cur?.assigned_by ?? "";
1860
- if (curStatus === "open" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
2059
+ const claimExpired = isLeaseExpired(cur?.lease_expires_at ?? null, Date.parse(now));
2060
+ if ((curStatus === "open" || curStatus === "blocked") && claimedBySession && (claimExpired || !isTmuxSessionAlive(claimedBySession))) {
1861
2061
  process.stderr.write(
1862
- `[tasks] Auto-clearing dead open-task pre-bind on ${taskId} (was ${claimedBySession})
2062
+ `[tasks] Auto-clearing dead/expired ${curStatus}-task pre-bind on ${taskId} (was ${claimedBySession})
1863
2063
  `
1864
2064
  );
1865
2065
  await client.execute({
1866
2066
  sql: `UPDATE tasks
1867
2067
  SET assigned_tmux = NULL,
1868
2068
  instance_id = NULL,
2069
+ claimed_by = NULL,
2070
+ claimed_at = NULL,
2071
+ last_heartbeat_at = NULL,
2072
+ lease_expires_at = NULL,
1869
2073
  updated_at = ?
1870
2074
  WHERE id = ?
1871
- AND status = 'open'
2075
+ AND status IN ('open', 'blocked')
1872
2076
  AND assigned_tmux = ?`,
1873
2077
  args: [now, taskId, claimedBySession]
1874
2078
  });
@@ -1963,49 +2167,158 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1963
2167
  return { row, taskFile, now, taskId, previousStatus, requestedStatus };
1964
2168
  }
1965
2169
  }
2170
+ const reclaimRoster = loadEmployeesSync();
2171
+ const callerInstance = instanceToken(input.callerSession ?? tmuxSession, reclaimRoster);
2172
+ const holderInstance = instanceToken(claimedBySession, reclaimRoster);
2173
+ const rowInstanceId = row.instance_id ? String(row.instance_id) : "";
2174
+ const ownerReclaim = curStatus === "in_progress" && !!input.callerAgentId && input.callerAgentId === String(row.assigned_to ?? "") && callerInstance !== null && holderInstance !== null && holderInstance === callerInstance && (rowInstanceId === "" || instanceToken(rowInstanceId, reclaimRoster) === callerInstance);
2175
+ if (ownerReclaim) {
2176
+ const nowMs = Date.parse(now);
2177
+ const churn = _ownerReclaimChurn.get(taskId);
2178
+ if (churn && nowMs - churn.windowStart <= OWNER_RECLAIM_CHURN_WINDOW_MS) {
2179
+ churn.count += 1;
2180
+ if (churn.count > OWNER_RECLAIM_MAX_CHURN) {
2181
+ process.stderr.write(
2182
+ `[tasks] Owner-reclaim CONTENTION on ${taskId}: ${churn.count} reclaims within ${OWNER_RECLAIM_CHURN_WINDOW_MS}ms \u2014 refusing to rebind; leaving holder ${claimedBySession} in place
2183
+ `
2184
+ );
2185
+ recordOrchestrationEventBestEffort({
2186
+ eventType: "claim.contention",
2187
+ source: "tasks-crud.updateTaskStatus",
2188
+ severity: "warn",
2189
+ taskId,
2190
+ agentId: eventAgentId,
2191
+ sessionScope: eventSessionScope,
2192
+ projectName: eventProjectName,
2193
+ tmuxSession,
2194
+ result: "owner_reclaim_livelock",
2195
+ payload: { reclaims: churn.count, holder: claimedBySession }
2196
+ });
2197
+ throw new Error(
2198
+ `${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is contended \u2014 ${churn.count} owner-reclaims within ${OWNER_RECLAIM_CHURN_WINDOW_MS}ms; leaving current holder ${claimedBySession} in place to break the rebind loop.`
2199
+ );
2200
+ }
2201
+ } else {
2202
+ if (_ownerReclaimChurn.size > 512) {
2203
+ for (const [k, v] of _ownerReclaimChurn) {
2204
+ if (nowMs - v.windowStart > OWNER_RECLAIM_CHURN_WINDOW_MS) _ownerReclaimChurn.delete(k);
2205
+ }
2206
+ }
2207
+ _ownerReclaimChurn.set(taskId, { count: 1, windowStart: nowMs });
2208
+ }
2209
+ process.stderr.write(
2210
+ `[tasks] Owner reclaim: ${input.callerAgentId} (${input.callerSession ?? tmuxSession}) reclaiming own task ${taskId} from live session ${claimedBySession}
2211
+ `
2212
+ );
2213
+ const reclaimed = await client.execute({
2214
+ sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ?,
2215
+ claimed_by = ?, claimed_at = ?, last_heartbeat_at = ?,
2216
+ lease_expires_at = ?,
2217
+ claim_count = COALESCE(claim_count, 0) + 1
2218
+ WHERE id = ? AND status = 'in_progress'
2219
+ AND assigned_to IS ? AND session_scope IS ? AND assigned_tmux IS ?`,
2220
+ args: [
2221
+ tmuxSession,
2222
+ now,
2223
+ claimAgent,
2224
+ now,
2225
+ now,
2226
+ leaseExpiry,
2227
+ taskId,
2228
+ row.assigned_to != null ? String(row.assigned_to) : null,
2229
+ eventSessionScope,
2230
+ cur?.assigned_tmux ?? null
2231
+ ]
2232
+ });
2233
+ if (reclaimed.rowsAffected > 0) {
2234
+ try {
2235
+ await writeCheckpoint({
2236
+ taskId,
2237
+ step: "owner_reclaim",
2238
+ contextSummary: `Task reclaimed by true owner ${input.callerAgentId} from live session ${claimedBySession}.`
2239
+ });
2240
+ } catch {
2241
+ }
2242
+ recordOrchestrationEventBestEffort({
2243
+ eventType: "task.claimed",
2244
+ source: "tasks-crud.updateTaskStatus",
2245
+ taskId,
2246
+ agentId: eventAgentId,
2247
+ sessionScope: eventSessionScope,
2248
+ projectName: eventProjectName,
2249
+ tmuxSession,
2250
+ result: "owner_reclaim",
2251
+ payload: { previousAssignedTmux: claimedBySession }
2252
+ });
2253
+ recordOrchestrationEventBestEffort({
2254
+ eventType: "task.in_progress",
2255
+ source: "tasks-crud.updateTaskStatus",
2256
+ taskId,
2257
+ agentId: eventAgentId,
2258
+ sessionScope: eventSessionScope,
2259
+ projectName: eventProjectName,
2260
+ tmuxSession,
2261
+ result: "owner_reclaim"
2262
+ });
2263
+ return { row, taskFile, now, taskId, previousStatus, requestedStatus };
2264
+ }
2265
+ }
1966
2266
  if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
1967
2267
  process.stderr.write(
1968
2268
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
1969
2269
  `
1970
2270
  );
1971
- await client.execute({
2271
+ const overridden = await client.execute({
1972
2272
  sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ?,
1973
2273
  claimed_by = ?, claimed_at = ?, last_heartbeat_at = ?,
1974
2274
  lease_expires_at = ?,
1975
2275
  claim_count = COALESCE(claim_count, 0) + 1
1976
- WHERE id = ?`,
1977
- args: [tmuxSession, now, claimAgent, now, now, leaseExpiry, taskId]
2276
+ WHERE id = ? AND status = 'in_progress'
2277
+ AND assigned_by IS ? AND session_scope IS ?`,
2278
+ args: [
2279
+ tmuxSession,
2280
+ now,
2281
+ claimAgent,
2282
+ now,
2283
+ now,
2284
+ leaseExpiry,
2285
+ taskId,
2286
+ cur?.assigned_by ?? null,
2287
+ eventSessionScope
2288
+ ]
1978
2289
  });
1979
- try {
1980
- await writeCheckpoint({
2290
+ if (overridden.rowsAffected > 0) {
2291
+ try {
2292
+ await writeCheckpoint({
2293
+ taskId,
2294
+ step: "assigner_override",
2295
+ contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
2296
+ });
2297
+ } catch {
2298
+ }
2299
+ recordOrchestrationEventBestEffort({
2300
+ eventType: "task.claimed",
2301
+ source: "tasks-crud.updateTaskStatus",
1981
2302
  taskId,
1982
- step: "assigner_override",
1983
- contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
2303
+ agentId: eventAgentId,
2304
+ sessionScope: eventSessionScope,
2305
+ projectName: eventProjectName,
2306
+ tmuxSession,
2307
+ result: "assigner_override",
2308
+ payload: { callerAgentId: input.callerAgentId }
1984
2309
  });
1985
- } catch {
2310
+ recordOrchestrationEventBestEffort({
2311
+ eventType: "task.in_progress",
2312
+ source: "tasks-crud.updateTaskStatus",
2313
+ taskId,
2314
+ agentId: eventAgentId,
2315
+ sessionScope: eventSessionScope,
2316
+ projectName: eventProjectName,
2317
+ tmuxSession,
2318
+ result: "assigner_override"
2319
+ });
2320
+ return { row, taskFile, now, taskId, previousStatus, requestedStatus };
1986
2321
  }
1987
- recordOrchestrationEventBestEffort({
1988
- eventType: "task.claimed",
1989
- source: "tasks-crud.updateTaskStatus",
1990
- taskId,
1991
- agentId: eventAgentId,
1992
- sessionScope: eventSessionScope,
1993
- projectName: eventProjectName,
1994
- tmuxSession,
1995
- result: "assigner_override",
1996
- payload: { callerAgentId: input.callerAgentId }
1997
- });
1998
- recordOrchestrationEventBestEffort({
1999
- eventType: "task.in_progress",
2000
- source: "tasks-crud.updateTaskStatus",
2001
- taskId,
2002
- agentId: eventAgentId,
2003
- sessionScope: eventSessionScope,
2004
- projectName: eventProjectName,
2005
- tmuxSession,
2006
- result: "assigner_override"
2007
- });
2008
- return { row, taskFile, now, taskId, previousStatus, requestedStatus };
2009
2322
  }
2010
2323
  const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
2011
2324
  recordOrchestrationEventBestEffort({
@@ -2139,12 +2452,42 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2139
2452
  }
2140
2453
  }
2141
2454
  }
2142
- const buildStatusStatement = () => input.result ? {
2143
- sql: `UPDATE tasks SET status = ?, result = ?, updated_at = ?${actualMinutesClause} WHERE id = ?`,
2144
- args: [input.status, input.result, now, ...actualMinutesArgs, taskId]
2145
- } : {
2146
- sql: `UPDATE tasks SET status = ?, updated_at = ?${actualMinutesClause} WHERE id = ?`,
2147
- args: [input.status, now, ...actualMinutesArgs, taskId]
2455
+ if ((input.status === "needs_review" || input.status === "closed" || input.status === "cancelled") && (previousStatus === "in_progress" || previousStatus === "blocked")) {
2456
+ const claimHolder = row.assigned_tmux ? String(row.assigned_tmux) : "";
2457
+ const leaseExpired = isLeaseExpired(
2458
+ row.lease_expires_at ? String(row.lease_expires_at) : null,
2459
+ Date.parse(now)
2460
+ );
2461
+ if (claimHolder && (leaseExpired || !isTmuxSessionAlive(claimHolder))) {
2462
+ process.stderr.write(
2463
+ `[tasks] Clearing dead/expired lease on completing task ${taskId} (was ${claimHolder})
2464
+ `
2465
+ );
2466
+ await client.execute({
2467
+ sql: `UPDATE tasks SET assigned_tmux = NULL, lease_expires_at = NULL, updated_at = ?
2468
+ WHERE id = ? AND assigned_tmux = ?`,
2469
+ args: [now, taskId, claimHolder]
2470
+ });
2471
+ }
2472
+ }
2473
+ const buildStatusStatement = () => {
2474
+ const hasResult = typeof input.result === "string" && input.result.trim().length > 0;
2475
+ if (!hasResult) {
2476
+ return {
2477
+ sql: `UPDATE tasks SET status = ?, updated_at = ?${actualMinutesClause} WHERE id = ?`,
2478
+ args: [input.status, now, ...actualMinutesArgs, taskId]
2479
+ };
2480
+ }
2481
+ if (input.preserveResultIfPresent) {
2482
+ return {
2483
+ sql: `UPDATE tasks SET status = ?, result = CASE WHEN result IS NULL OR trim(result, char(32)||char(9)||char(10)||char(13)) = '' THEN ? ELSE result END, updated_at = ?${actualMinutesClause} WHERE id = ?`,
2484
+ args: [input.status, input.result, now, ...actualMinutesArgs, taskId]
2485
+ };
2486
+ }
2487
+ return {
2488
+ sql: `UPDATE tasks SET status = ?, result = ?, updated_at = ?${actualMinutesClause} WHERE id = ?`,
2489
+ args: [input.status, input.result, now, ...actualMinutesArgs, taskId]
2490
+ };
2148
2491
  };
2149
2492
  const writeStatements = [];
2150
2493
  if (structuredSerialized) {
@@ -3007,21 +3350,25 @@ function isStale(isoTimestamp) {
3007
3350
  if (!isoTimestamp) return false;
3008
3351
  return Date.now() - new Date(isoTimestamp).getTime() > STALE_REVIEW_AGE_MS;
3009
3352
  }
3010
- async function countPendingReviews(sessionScope, reviewer) {
3011
- const extraConditions = reviewer ? ["reviewer = ?"] : void 0;
3012
- const extraArgs = reviewer ? [reviewer] : void 0;
3013
- const rows = await queryTaskRows({
3353
+ function pendingReviewsCountOpts(sessionScope, reviewer) {
3354
+ return {
3014
3355
  status: "needs_review",
3015
3356
  columns: "COUNT(*) as cnt",
3016
3357
  sessionScope: sessionScope ?? null,
3017
3358
  strictSession: sessionScope !== void 0,
3018
3359
  projectName: null,
3019
3360
  // no project filter for review counts
3020
- extraConditions,
3021
- extraArgs,
3361
+ extraConditions: reviewer ? ["reviewer = ?"] : void 0,
3362
+ extraArgs: reviewer ? [reviewer] : void 0,
3022
3363
  limit: 1,
3023
3364
  orderBy: "1"
3024
- });
3365
+ };
3366
+ }
3367
+ async function buildPendingReviewsCountStatement(sessionScope, reviewer) {
3368
+ return buildTaskRowsStatement(pendingReviewsCountOpts(sessionScope, reviewer));
3369
+ }
3370
+ async function countPendingReviews(sessionScope, reviewer) {
3371
+ const rows = await queryTaskRows(pendingReviewsCountOpts(sessionScope, reviewer));
3025
3372
  return Number(rows[0]?.cnt) || 0;
3026
3373
  }
3027
3374
  async function countNewPendingReviewsSince(sinceIso, sessionScope, reviewer) {
@@ -3346,7 +3693,10 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
3346
3693
  "- **Needs work:** re-open with notes"
3347
3694
  ].join("\n");
3348
3695
  const originalTaskId = String(row.id);
3349
- const updatedResult = (result ?? "No result summary provided") + reviewNotes;
3696
+ const persistedResult = row.result != null ? String(row.result) : "";
3697
+ const argResult = result != null ? String(result) : "";
3698
+ const baseResult = persistedResult.trim().length > 0 ? persistedResult : argResult.trim().length > 0 ? argResult : "No result summary provided";
3699
+ const updatedResult = baseResult + reviewNotes;
3350
3700
  await client.execute({
3351
3701
  sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
3352
3702
  WHERE id = ?`,
@@ -3477,10 +3827,27 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3477
3827
  }
3478
3828
 
3479
3829
  // src/lib/worktree-guard.ts
3480
- import { execFileSync } from "child_process";
3481
- import { existsSync as existsSync8 } from "fs";
3830
+ import { execFile, execFileSync } from "child_process";
3831
+ import { promisify as promisify2 } from "util";
3832
+ import { existsSync as existsSync8, realpathSync } from "fs";
3482
3833
  import path7 from "path";
3483
3834
  var GIT_TIMEOUT_MS = 1e4;
3835
+ var execFileAsync2 = promisify2(execFile);
3836
+ function realpath(p) {
3837
+ try {
3838
+ return realpathSync(p);
3839
+ } catch {
3840
+ return p;
3841
+ }
3842
+ }
3843
+ var defaultGitExecutor = async (args, cwd) => {
3844
+ const { stdout } = await execFileAsync2("git", args, {
3845
+ cwd,
3846
+ encoding: "utf8",
3847
+ timeout: GIT_TIMEOUT_MS
3848
+ });
3849
+ return String(stdout ?? "");
3850
+ };
3484
3851
  function listWorktreesPorcelain(repoRoot) {
3485
3852
  try {
3486
3853
  const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
@@ -3561,6 +3928,90 @@ function checkWorktreeCollisions(repoPath) {
3561
3928
  }
3562
3929
  return warnings;
3563
3930
  }
3931
+ async function getGitRootAsync(dir, git) {
3932
+ try {
3933
+ const root = (await git(["rev-parse", "--show-toplevel"], dir)).trim();
3934
+ return root ? realpath(root) : null;
3935
+ } catch {
3936
+ return null;
3937
+ }
3938
+ }
3939
+ async function listWorktreesPorcelainAsync(repoRoot, git) {
3940
+ try {
3941
+ const output = await git(["worktree", "list", "--porcelain"], repoRoot);
3942
+ const worktrees = [];
3943
+ let currentPath = "";
3944
+ for (const line of output.split("\n")) {
3945
+ if (line.startsWith("worktree ")) {
3946
+ currentPath = line.slice("worktree ".length);
3947
+ } else if (line.startsWith("branch ") && currentPath) {
3948
+ const branch = line.slice("branch refs/heads/".length);
3949
+ worktrees.push({ path: currentPath, branch });
3950
+ currentPath = "";
3951
+ }
3952
+ }
3953
+ return worktrees;
3954
+ } catch {
3955
+ return [];
3956
+ }
3957
+ }
3958
+ async function isWorktreeDirtyAsync(wtPath, git) {
3959
+ try {
3960
+ return (await git(["status", "--porcelain"], wtPath)).trim().length > 0;
3961
+ } catch {
3962
+ return true;
3963
+ }
3964
+ }
3965
+ async function hasUnpushedCommitsAsync(wtPath, branch, git) {
3966
+ for (const mainRef of ["main", "master"]) {
3967
+ try {
3968
+ const ahead = (await git(["-C", wtPath, "rev-list", `${mainRef}..${branch}`, "--count"], wtPath)).trim();
3969
+ return parseInt(ahead, 10) > 0;
3970
+ } catch {
3971
+ }
3972
+ }
3973
+ return true;
3974
+ }
3975
+ async function checkWorktreeCollisionsAsync(repoPath, git = defaultGitExecutor) {
3976
+ const worktreeListCache = /* @__PURE__ */ new Map();
3977
+ const listWorktreesOnce = (root) => {
3978
+ let cached = worktreeListCache.get(root);
3979
+ if (!cached) {
3980
+ cached = listWorktreesPorcelainAsync(root, git);
3981
+ worktreeListCache.set(root, cached);
3982
+ }
3983
+ return cached;
3984
+ };
3985
+ const repoRoot = await getGitRootAsync(repoPath, git);
3986
+ if (!repoRoot) return [];
3987
+ const warnings = [];
3988
+ const worktrees = await listWorktreesOnce(repoRoot);
3989
+ for (const wt of worktrees) {
3990
+ if (!wt.path.includes("/.worktrees/")) continue;
3991
+ if (!existsSync8(wt.path)) continue;
3992
+ const owner = inferSessionOwner(wt.path, wt.branch);
3993
+ const dirty = await isWorktreeDirtyAsync(wt.path, git);
3994
+ const hasCommits = await hasUnpushedCommitsAsync(wt.path, wt.branch, git);
3995
+ if (dirty) {
3996
+ warnings.push({
3997
+ worktreePath: wt.path,
3998
+ branch: wt.branch,
3999
+ sessionOwner: owner,
4000
+ dirty: true,
4001
+ reason: `Worktree has uncommitted changes (agent: ${owner ?? "unknown"}, branch: ${wt.branch})`
4002
+ });
4003
+ } else if (hasCommits) {
4004
+ warnings.push({
4005
+ worktreePath: wt.path,
4006
+ branch: wt.branch,
4007
+ sessionOwner: owner,
4008
+ dirty: false,
4009
+ reason: `Worktree has unpushed commits on branch ${wt.branch} (agent: ${owner ?? "unknown"})`
4010
+ });
4011
+ }
4012
+ }
4013
+ return warnings;
4014
+ }
3564
4015
 
3565
4016
  // src/lib/intercom-prompts.ts
3566
4017
  function formatIntercomPrompt(reason = "nudge") {
@@ -3578,6 +4029,15 @@ function formatIntercomPrompt(reason = "nudge") {
3578
4029
  }
3579
4030
  }
3580
4031
 
4032
+ // src/lib/session-scope-token.ts
4033
+ var VALID_SESSION_SCOPE_RE = /^[a-zA-Z0-9_]+$/;
4034
+ function isValidSessionScope(scope) {
4035
+ return typeof scope === "string" && scope.length > 0 && scope.length <= 64 && VALID_SESSION_SCOPE_RE.test(scope);
4036
+ }
4037
+ function sanitizeSessionScope(scope) {
4038
+ return isValidSessionScope(scope) ? scope : null;
4039
+ }
4040
+
3581
4041
  // src/lib/tmux-routing.ts
3582
4042
  var _cachedExeBinDir;
3583
4043
  function resolveExeBinDir() {
@@ -3618,9 +4078,11 @@ function buildSpawnPath(inheritedPath = process.env.PATH, execPath = process.exe
3618
4078
  }
3619
4079
  return ordered.join(sep);
3620
4080
  }
3621
- var SPAWN_LOCK_DIR = path8.join(os6.homedir(), ".exe-os", "spawn-locks");
4081
+ function spawnLockDir() {
4082
+ return process.env.EXE_SPAWN_LOCK_DIR || path8.join(os6.homedir(), ".exe-os", "spawn-locks");
4083
+ }
3622
4084
  function spawnLockPath(sessionName) {
3623
- return path8.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4085
+ return path8.join(spawnLockDir(), `${sessionName}.lock`);
3624
4086
  }
3625
4087
  function isProcessAlive(pid) {
3626
4088
  try {
@@ -3631,8 +4093,9 @@ function isProcessAlive(pid) {
3631
4093
  }
3632
4094
  }
3633
4095
  function acquireSpawnLock(sessionName) {
3634
- if (!existsSync9(SPAWN_LOCK_DIR)) {
3635
- mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
4096
+ const lockDir = spawnLockDir();
4097
+ if (!existsSync9(lockDir)) {
4098
+ mkdirSync6(lockDir, { recursive: true });
3636
4099
  }
3637
4100
  const lockFile = spawnLockPath(sessionName);
3638
4101
  const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
@@ -3743,7 +4206,7 @@ async function exportBehaviorsAsync(agentId, projectName, sessionKey) {
3743
4206
  const script = resolveBehaviorsExporterScript();
3744
4207
  if (!script) return null;
3745
4208
  try {
3746
- const { stdout } = await execFileAsync(
4209
+ const { stdout } = await execFileAsync3(
3747
4210
  process.execPath,
3748
4211
  [script, agentId, projectName, sessionKey],
3749
4212
  { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
@@ -3824,6 +4287,13 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3824
4287
  mkdirSync6(SESSION_CACHE, { recursive: true });
3825
4288
  }
3826
4289
  const rootExe = extractRootExe(parentExe) ?? parentExe;
4290
+ if (!isValidSessionScope(rootExe)) {
4291
+ process.stderr.write(
4292
+ `[tmux-routing] ERROR: refusing to cache corrupted parent session ${JSON.stringify(String(parentExe).slice(0, 60))} for key "${sessionKey}" \u2014 not a valid session token (bug 4941abba).
4293
+ `
4294
+ );
4295
+ return;
4296
+ }
3827
4297
  const filePath = path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3828
4298
  atomicWriteJsonSync(filePath, {
3829
4299
  parentExe: rootExe,
@@ -3858,14 +4328,28 @@ function getDispatchedBy(sessionKey) {
3858
4328
  function resolveExeSession() {
3859
4329
  const mySession = getMySession();
3860
4330
  const fromSessionName = mySession ? extractRootExe(mySession) ?? null : null;
3861
- const alsHint = (() => {
4331
+ const agentCtx = (() => {
3862
4332
  try {
3863
- return getAgentContext()?.sessionHint ?? "";
4333
+ return getAgentContext();
3864
4334
  } catch {
3865
- return "";
4335
+ return void 0;
3866
4336
  }
3867
4337
  })();
3868
- const envHint = process.env.EXE_SESSION_NAME || "";
4338
+ const inDaemonRequest = agentCtx !== void 0;
4339
+ const alsHint = agentCtx?.sessionHint ?? "";
4340
+ if (agentCtx && !alsHint) {
4341
+ logSessionResolve({
4342
+ caller: "resolveExeSession",
4343
+ mySession,
4344
+ alsHint: "",
4345
+ envHint: process.env.EXE_SESSION_NAME || "",
4346
+ cacheHit: null,
4347
+ resolved: null,
4348
+ path: "daemon_request_no_hint (daemon env/tmux residue refused)"
4349
+ });
4350
+ return null;
4351
+ }
4352
+ const envHint = inDaemonRequest ? "" : process.env.EXE_SESSION_NAME || "";
3869
4353
  const validateLiveHint = (hint, source) => {
3870
4354
  if (!hint || !isRootSession(hint)) return hint || null;
3871
4355
  try {
@@ -3895,7 +4379,17 @@ function resolveExeSession() {
3895
4379
  return hint;
3896
4380
  }
3897
4381
  };
3898
- const sessionHintRaw = alsHint || envHint || "";
4382
+ let sessionHintRaw = alsHint || envHint || "";
4383
+ if (sessionHintRaw) {
4384
+ const hintRoot = extractRootExe(sessionHintRaw) ?? sessionHintRaw;
4385
+ if (!isValidSessionScope(hintRoot)) {
4386
+ process.stderr.write(
4387
+ `[tmux-routing] ERROR: ignoring corrupted session hint ${JSON.stringify(String(sessionHintRaw).slice(0, 60))} (source=${alsHint ? "ALS" : "env"}) \u2014 not a valid session token (bug 4941abba); using tmux/cache resolution.
4388
+ `
4389
+ );
4390
+ sessionHintRaw = "";
4391
+ }
4392
+ }
3899
4393
  if (sessionHintRaw) {
3900
4394
  const fromEnv = extractRootExe(sessionHintRaw) ?? sessionHintRaw;
3901
4395
  if (fromEnv) {
@@ -4098,7 +4592,7 @@ async function verifyPaneAtCapacity(sessionName) {
4098
4592
  reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
4099
4593
  };
4100
4594
  }
4101
- const { isAtCapacity } = await import("./capacity-monitor-S54IOWRL.js");
4595
+ const { isAtCapacity } = await import("./capacity-monitor-ZG5GGFR2.js");
4102
4596
  if (!isAtCapacity(pane)) {
4103
4597
  return {
4104
4598
  atCapacity: false,
@@ -4114,7 +4608,7 @@ var INTERCOM_DEBOUNCE_MS = 3e5;
4114
4608
  var CODEX_DEBOUNCE_MS = 3e5;
4115
4609
  var INTERCOM_LOG2 = path8.join(os6.homedir(), ".exe-os", "intercom.log");
4116
4610
  var INTERCOM_LOG_MAX_BYTES2 = 1024 * 1024;
4117
- var execFileAsync = promisify(execFile);
4611
+ var execFileAsync3 = promisify3(execFile2);
4118
4612
  var DEBOUNCE_FILE = path8.join(SESSION_CACHE, "intercom-debounce.json");
4119
4613
  var DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4120
4614
  function readDebounceState() {
@@ -4193,24 +4687,47 @@ function logIntercom(msg, meta) {
4193
4687
  } catch {
4194
4688
  }
4195
4689
  }
4196
- var BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
4690
+ function classifyPaneSessionState(pane) {
4691
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4692
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4693
+ return "no_claude";
4694
+ }
4695
+ }
4696
+ if (/Running…/.test(pane)) return "tool";
4697
+ if (BUSY_PATTERN.test(pane)) return "thinking";
4698
+ return "idle";
4699
+ }
4197
4700
  function getSessionState(sessionName) {
4198
4701
  const transport = getTransport();
4199
4702
  if (!transport.isAlive(sessionName)) return "offline";
4200
4703
  try {
4201
- const pane = transport.capturePane(sessionName, 5);
4202
- if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4203
- if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4204
- return "no_claude";
4205
- }
4206
- }
4207
- if (/Running…/.test(pane)) return "tool";
4208
- if (BUSY_PATTERN.test(pane)) return "thinking";
4209
- return "idle";
4704
+ return classifyPaneSessionState(transport.capturePane(sessionName, 5));
4705
+ } catch {
4706
+ return "offline";
4707
+ }
4708
+ }
4709
+ async function captureSessionPaneAsync(sessionName, lines) {
4710
+ const { stdout } = await execFileAsync3(
4711
+ "tmux",
4712
+ ["capture-pane", "-t", sessionName, "-p", "-S", `-${lines}`],
4713
+ { encoding: "utf8", timeout: 3e3 }
4714
+ );
4715
+ return String(stdout ?? "");
4716
+ }
4717
+ async function getSessionStateAsync(sessionName) {
4718
+ try {
4719
+ return classifyPaneSessionState(await captureSessionPaneAsync(sessionName, 5));
4210
4720
  } catch {
4211
4721
  return "offline";
4212
4722
  }
4213
4723
  }
4724
+ async function sessionHasPendingInputAsync(sessionName) {
4725
+ try {
4726
+ return paneHasPendingInput(await captureSessionPaneAsync(sessionName, 12));
4727
+ } catch {
4728
+ return false;
4729
+ }
4730
+ }
4214
4731
  function isSessionBusy(sessionName) {
4215
4732
  const state = getSessionState(sessionName);
4216
4733
  return state === "thinking" || state === "tool";
@@ -4253,7 +4770,7 @@ function sessionHasPendingInput(sessionName) {
4253
4770
  }
4254
4771
  }
4255
4772
  function decideQueuedNudgeDrain(intercomReason, state) {
4256
- if (state.hasInProgressTask) {
4773
+ if (state.hasInProgressTask && state.isIdle === false) {
4257
4774
  return { action: "drop", reason: "in_progress_task_will_auto_chain" };
4258
4775
  }
4259
4776
  if (intercomReason === "message" && !state.hasUnreadMessages) {
@@ -4276,6 +4793,111 @@ async function countScopedPendingReviews(targetSession, reviewer) {
4276
4793
  return null;
4277
4794
  }
4278
4795
  }
4796
+ async function countScopedOpenTasks(targetSession, agentId) {
4797
+ try {
4798
+ const { fastDbInit } = await import("./fast-db-init-4CWHSOOR.js");
4799
+ const client = await fastDbInit();
4800
+ const scope = extractRootExe(targetSession);
4801
+ if (!scope) return 0;
4802
+ const rawAgent = agentId ?? targetSession.split("-")[0] ?? targetSession;
4803
+ const baseAgent = baseAgentName(rawAgent);
4804
+ const agentIds = rawAgent !== baseAgent ? [rawAgent, baseAgent] : [baseAgent];
4805
+ const placeholders = agentIds.map(() => "?").join(", ");
4806
+ const result = await client.execute({
4807
+ sql: `SELECT COUNT(*) as cnt
4808
+ FROM tasks
4809
+ WHERE assigned_to IN (${placeholders})
4810
+ AND status = 'open'
4811
+ AND session_scope = ?
4812
+ AND (instance_id IS NULL OR instance_id = ?)
4813
+ AND (assigned_tmux IS NULL OR assigned_tmux = 'unknown' OR assigned_tmux = ? OR assigned_tmux LIKE ?)`,
4814
+ args: [...agentIds, scope, targetSession, targetSession, `${targetSession}:%`]
4815
+ });
4816
+ return Number(result.rows[0]?.cnt ?? 0);
4817
+ } catch {
4818
+ return null;
4819
+ }
4820
+ }
4821
+ function shouldDispositionWedged(input) {
4822
+ const rawAgent = input.targetSession.split("-")[0] ?? input.targetSession;
4823
+ if (isExeSession(input.targetSession) || !input.targetSession.includes("-") || isCoordinatorName(rawAgent)) {
4824
+ return false;
4825
+ }
4826
+ if (input.state === "offline" || input.state === "no_claude") return true;
4827
+ if (input.hasPendingInput && input.state !== "thinking" && input.state !== "tool") return true;
4828
+ return false;
4829
+ }
4830
+ function buildScopedInProgressTaskQuery(targetSession) {
4831
+ if (isExeSession(targetSession) || !targetSession.includes("-")) return null;
4832
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
4833
+ if (isCoordinatorName(rawAgent)) return null;
4834
+ const scope = extractRootExe(targetSession);
4835
+ if (!scope) return null;
4836
+ const baseAgent = baseAgentName(rawAgent);
4837
+ const agentIds = rawAgent !== baseAgent ? [rawAgent, baseAgent] : [baseAgent];
4838
+ const placeholders = agentIds.map(() => "?").join(", ");
4839
+ return {
4840
+ sql: `SELECT id FROM tasks
4841
+ WHERE assigned_to IN (${placeholders})
4842
+ AND status = 'in_progress'
4843
+ AND session_scope = ?
4844
+ AND (assigned_tmux = ? OR assigned_tmux LIKE ?)
4845
+ ORDER BY updated_at ASC
4846
+ LIMIT 1`,
4847
+ args: [...agentIds, scope, targetSession, `${targetSession}:%`]
4848
+ };
4849
+ }
4850
+ async function findScopedInProgressTaskId(targetSession) {
4851
+ const q = buildScopedInProgressTaskQuery(targetSession);
4852
+ if (!q) return null;
4853
+ const { fastDbInit } = await import("./fast-db-init-4CWHSOOR.js");
4854
+ const client = await fastDbInit();
4855
+ const result = await client.execute(q);
4856
+ const id = result.rows[0]?.id;
4857
+ return id == null ? null : String(id);
4858
+ }
4859
+ function offlineIfEmpty(pane, classified) {
4860
+ return !pane || pane.trim() === "" ? "offline" : classified;
4861
+ }
4862
+ async function dispositionWedgedSessionTask(targetSession, reason, wedge, deps) {
4863
+ if (!shouldDispositionWedged({ targetSession, state: wedge.state, hasPendingInput: wedge.hasPendingInput })) {
4864
+ return "not_wedged";
4865
+ }
4866
+ try {
4867
+ const findTaskId = deps?.findTaskId ?? findScopedInProgressTaskId;
4868
+ const blockTask = deps?.blockTask ?? (async (taskId2, r) => {
4869
+ const { updateTask } = await import("./lib/tasks.js");
4870
+ await updateTask({ taskId: taskId2, status: "blocked", result: r, preserveResultIfPresent: true });
4871
+ });
4872
+ const taskId = await findTaskId(targetSession);
4873
+ if (taskId == null) return "no_task";
4874
+ await blockTask(taskId, reason);
4875
+ logIntercom(`DISPOSITIONED \u2192 ${targetSession} (task ${taskId.slice(0, 8)} \u2192 blocked: ${reason})`);
4876
+ return "dispositioned";
4877
+ } catch (e) {
4878
+ process.stderr.write(
4879
+ `[tmux-routing] dispositionWedgedSessionTask ${targetSession}: ${e instanceof Error ? e.message : String(e)}
4880
+ `
4881
+ );
4882
+ return "error";
4883
+ }
4884
+ }
4885
+ async function decideDrainSend(item, ctx) {
4886
+ const state = offlineIfEmpty(ctx.pane, ctx.paneState);
4887
+ const decision = decideQueuedNudgeDrain(item.intercomReason, ctx.nudgeState);
4888
+ if (decision.action === "drop") {
4889
+ const outcome = await ctx.disposition(
4890
+ item.targetSession,
4891
+ `Auto-blocked: queued ${item.intercomReason ?? "nudge"} intercom was undeliverable and revalidated stale (${decision.reason}) while the pane is wedged (state=${state}, pendingInput=${ctx.hasPendingInput}; ${item.reason}). Re-dispatch after clearing the pane.`,
4892
+ { state, hasPendingInput: ctx.hasPendingInput }
4893
+ );
4894
+ if (outcome === "dispositioned") return true;
4895
+ if (outcome === "error") return false;
4896
+ return true;
4897
+ }
4898
+ const result = await ctx.deliver(item.targetSession, item.intercomReason ?? "nudge");
4899
+ return result === "delivered" || result === "debounced" || result === "skipped_exe";
4900
+ }
4279
4901
  function sendIntercom(targetSession, opts) {
4280
4902
  const transport = getTransport();
4281
4903
  const startedAt = Date.now();
@@ -4346,6 +4968,11 @@ function sendIntercom(targetSession, opts) {
4346
4968
  }
4347
4969
  } catch {
4348
4970
  }
4971
+ if ((process.env.EXE_NOTIFICATION_LEDGER ?? "").trim().toLowerCase() === "shadow") {
4972
+ const shadowScope = opts && "callerSessionScope" in opts ? opts.callerSessionScope : void 0;
4973
+ void import("./notification-ledger-SZEPGJQ7.js").then((m) => m.recordShadowNudge(targetSession, reason, shadowScope)).catch(() => {
4974
+ });
4975
+ }
4349
4976
  if (!opts?.force && isDebounced(targetSession)) {
4350
4977
  logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
4351
4978
  recordResult("debounced");
@@ -4464,7 +5091,7 @@ function scheduleDeferredSpawnNudge(input) {
4464
5091
  return;
4465
5092
  }
4466
5093
  queueIntercom(input.sessionName, `spawn nudge failed after ${attempt} attempts: ${msg}`, "signal");
4467
- void import("./notifications-KONB3C62.js").then(({ writeNotification: writeNotification2 }) => writeNotification2({
5094
+ void import("./notifications-4UT53CDG.js").then(({ writeNotification: writeNotification2 }) => writeNotification2({
4468
5095
  agentId: getCoordinatorName(),
4469
5096
  agentRole: "COO",
4470
5097
  event: "error_spike",
@@ -4484,7 +5111,7 @@ async function sendKeysLiteralAsync(targetSession, text, runtime) {
4484
5111
  const isCodex = runtime === "codex";
4485
5112
  const submit = async () => {
4486
5113
  await new Promise((r) => setTimeout(r, isCodex ? CODEX_SUBMIT_DELAY_MS : SUBMIT_DELAY_MS));
4487
- await execFileAsync("tmux", ["send-keys", "-t", targetSession, isCodex ? "C-m" : "Enter"], { timeout: 1e3 });
5114
+ await execFileAsync3("tmux", ["send-keys", "-t", targetSession, isCodex ? "C-m" : "Enter"], { timeout: 1e3 });
4488
5115
  };
4489
5116
  if (text.length >= 500) {
4490
5117
  const tmpDir = path8.join(os6.tmpdir(), "exe-os-intercom");
@@ -4492,8 +5119,8 @@ async function sendKeysLiteralAsync(targetSession, text, runtime) {
4492
5119
  const tmpFile = path8.join(tmpDir, `msg-${process.pid}-${Date.now()}.txt`);
4493
5120
  try {
4494
5121
  writeFileSync5(tmpFile, text, "utf-8");
4495
- await execFileAsync("tmux", ["load-buffer", tmpFile], { timeout: 3e3 });
4496
- await execFileAsync("tmux", ["paste-buffer", "-p", "-t", targetSession], { timeout: 3e3 });
5122
+ await execFileAsync3("tmux", ["load-buffer", tmpFile], { timeout: 3e3 });
5123
+ await execFileAsync3("tmux", ["paste-buffer", "-p", "-t", targetSession], { timeout: 3e3 });
4497
5124
  await submit();
4498
5125
  } finally {
4499
5126
  try {
@@ -4503,7 +5130,7 @@ async function sendKeysLiteralAsync(targetSession, text, runtime) {
4503
5130
  }
4504
5131
  return;
4505
5132
  }
4506
- await execFileAsync("tmux", ["send-keys", "-t", targetSession, "-l", text], { timeout: 3e3 });
5133
+ await execFileAsync3("tmux", ["send-keys", "-t", targetSession, "-l", text], { timeout: 3e3 });
4507
5134
  await submit();
4508
5135
  }
4509
5136
  async function sendIntercomAsync(targetSession, opts) {
@@ -4560,7 +5187,7 @@ async function sendIntercomAsync(targetSession, opts) {
4560
5187
  return "debounced";
4561
5188
  }
4562
5189
  try {
4563
- const { stdout } = await execFileAsync("tmux", ["list-sessions", "-F", "#{session_name}"], { encoding: "utf8", timeout: 3e3 });
5190
+ const { stdout } = await execFileAsync3("tmux", ["list-sessions", "-F", "#{session_name}"], { encoding: "utf8", timeout: 3e3 });
4564
5191
  const sessions = stdout.trim().split("\n").filter(Boolean);
4565
5192
  if (!sessions.includes(targetSession)) {
4566
5193
  logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
@@ -4574,7 +5201,7 @@ async function sendIntercomAsync(targetSession, opts) {
4574
5201
  recordResult("queued", "coordinator_no_inject");
4575
5202
  return "queued";
4576
5203
  }
4577
- const sessionState = getSessionState(targetSession);
5204
+ const sessionState = await getSessionStateAsync(targetSession);
4578
5205
  if (sessionState === "no_claude") {
4579
5206
  queueIntercom(targetSession, "claude not running in session", reason);
4580
5207
  const batched2 = recordDebounce(targetSession);
@@ -4590,14 +5217,14 @@ async function sendIntercomAsync(targetSession, opts) {
4590
5217
  return "queued";
4591
5218
  }
4592
5219
  try {
4593
- const mode = await execFileAsync("tmux", ["display-message", "-p", "-t", targetSession, "#{pane_in_mode}"], { encoding: "utf8", timeout: 1e3 });
5220
+ const mode = await execFileAsync3("tmux", ["display-message", "-p", "-t", targetSession, "#{pane_in_mode}"], { encoding: "utf8", timeout: 1e3 });
4594
5221
  if (mode.stdout.trim() === "1") {
4595
5222
  logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
4596
- await execFileAsync("tmux", ["send-keys", "-t", targetSession, "q"], { timeout: 1e3 });
5223
+ await execFileAsync3("tmux", ["send-keys", "-t", targetSession, "q"], { timeout: 1e3 });
4597
5224
  }
4598
5225
  } catch {
4599
5226
  }
4600
- if (sessionHasPendingInput(targetSession)) {
5227
+ if (await sessionHasPendingInputAsync(targetSession)) {
4601
5228
  queueIntercom(targetSession, "non-empty input buffer at send time", reason);
4602
5229
  const batched2 = recordDebounce(targetSession);
4603
5230
  logIntercom(`QUEUED \u2192 ${targetSession} (non-empty input buffer)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
@@ -4665,8 +5292,21 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
4665
5292
  return false;
4666
5293
  }
4667
5294
  }
4668
- async function ensureWorktreeAsync(projectDir, employeeName, instance) {
4669
- return ensureWorktree(projectDir, employeeName, instance);
5295
+ function emitOptOutBypassWarning(employeeName, projectDir) {
5296
+ process.stderr.write(
5297
+ `[tmux-routing] WORKTREE ISOLATION BYPASSED by explicit spawn_worktree=false for code-role ${employeeName} in real repo ${projectDir} \u2014 commits can land on main. This is a deliberate operator opt-out.
5298
+ `
5299
+ );
5300
+ }
5301
+ function worktreeIsolationGate(employeeName, projectDir, opts) {
5302
+ const nonGit = !isInsideGitRepo(projectDir);
5303
+ if (opts?.spawnWorktree === false) {
5304
+ if (!nonGit) emitOptOutBypassWarning(employeeName, projectDir);
5305
+ return { allow: true, nonGit };
5306
+ }
5307
+ if (nonGit) return { allow: true, nonGit: true };
5308
+ const message = `Worktree isolation required for code-role ${employeeName}; refusing to spawn in ${projectDir}. ${projectDir} is a git repo but the worktree could not be created (git error/timeout) \u2014 spawning here would let commits land on main. To dispatch a code-role agent here anyway, pass spawn_worktree=false (create_task) or opts.spawnWorktree=false.`;
5309
+ return { allow: false, message };
4670
5310
  }
4671
5311
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4672
5312
  employeeName = employeeName.toLowerCase();
@@ -4756,6 +5396,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4756
5396
  return { status: "failed", sessionName: failSession, error: compat.error };
4757
5397
  }
4758
5398
  }
5399
+ let acquiredHere = false;
4759
5400
  let effectiveInstance = opts?.instance;
4760
5401
  if (effectiveInstance === void 0 && opts?.autoInstance) {
4761
5402
  const free = findFreeInstance(
@@ -4770,6 +5411,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4770
5411
  error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
4771
5412
  };
4772
5413
  }
5414
+ acquiredHere = true;
4773
5415
  effectiveInstance = free === 0 ? void 0 : free;
4774
5416
  if (free !== null) {
4775
5417
  recordOrchestrationEventBestEffort({
@@ -4783,8 +5425,13 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4783
5425
  }
4784
5426
  }
4785
5427
  const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
5428
+ const releaseIfOwned = () => {
5429
+ if (acquiredHere) releaseSpawnLock(sessionName);
5430
+ };
4786
5431
  if (!opts?.autoInstance && !opts?.spawnLockHeld) {
4787
- if (!acquireSpawnLock(sessionName)) {
5432
+ if (acquireSpawnLock(sessionName)) {
5433
+ acquiredHere = true;
5434
+ } else {
4788
5435
  const reconcile = reconcileStaleSpawnLock(sessionName);
4789
5436
  if (reconcile.skip) {
4790
5437
  process.stderr.write(
@@ -4793,7 +5440,9 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4793
5440
  );
4794
5441
  return { status: "intercom_sent", sessionName };
4795
5442
  }
4796
- if (!acquireSpawnLock(sessionName)) {
5443
+ if (acquireSpawnLock(sessionName)) {
5444
+ acquiredHere = true;
5445
+ } else {
4797
5446
  process.stderr.write(
4798
5447
  `[ensureEmployee] spawn lock re-acquired by another process for ${sessionName} after stale-lock break \u2014 skipping duplicate spawn
4799
5448
  `
@@ -4803,8 +5452,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4803
5452
  }
4804
5453
  }
4805
5454
  if (isEmployeeAlive(sessionName)) {
4806
- if (!opts?.autoInstance) releaseSpawnLock(sessionName);
4807
- const result2 = sendIntercom(sessionName, { force: true, reason: "signal" });
5455
+ releaseIfOwned();
5456
+ const result2 = sendIntercom(sessionName, { force: true, reason: "signal", callerSessionScope: exeSession });
4808
5457
  if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
4809
5458
  return { status: "intercom_sent", sessionName };
4810
5459
  }
@@ -4846,10 +5495,36 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4846
5495
  process.stderr.write(`[tmux-routing] worktree isolation: ${employeeName} \u2192 ${wtPath}
4847
5496
  `);
4848
5497
  } else {
4849
- const msg = `Worktree isolation failed for code-role ${employeeName}; refusing to spawn in main checkout ${projectDir}`;
4850
- process.stderr.write(`[tmux-routing] ${msg}
5498
+ const gate = worktreeIsolationGate(employeeName, projectDir, opts);
5499
+ if (gate.allow) {
5500
+ spawnOpts.cwd = projectDir;
5501
+ spawnOpts.spawnWorktree = false;
5502
+ const reason = gate.nonGit ? "non_git_project" : "opt_out_override";
5503
+ process.stderr.write(
5504
+ `[tmux-routing] WORKTREE BYPASSED: code-role ${employeeName} spawning in ${projectDir} (reason=${reason}) \u2014 no git worktree isolation
5505
+ `
5506
+ );
5507
+ recordOrchestrationEventBestEffort({
5508
+ eventType: "worktree.bypassed",
5509
+ source: "tmux-routing.ensureEmployee",
5510
+ severity: "warn",
5511
+ agentId: employeeName,
5512
+ sessionScope: exeSession,
5513
+ instanceId: effectiveInstance != null ? String(effectiveInstance) : null,
5514
+ tmuxSession: sessionName,
5515
+ payload: {
5516
+ agent: employeeName,
5517
+ cwd: projectDir,
5518
+ reason,
5519
+ risk: "code-role agent runs in project root without git worktree isolation"
5520
+ }
5521
+ });
5522
+ } else {
5523
+ releaseIfOwned();
5524
+ process.stderr.write(`[tmux-routing] ${gate.message}
4851
5525
  `);
4852
- return { status: "failed", sessionName, error: msg };
5526
+ return { status: "failed", sessionName, error: gate.message };
5527
+ }
4853
5528
  }
4854
5529
  }
4855
5530
  } catch (e) {
@@ -4910,7 +5585,7 @@ async function ensureEmployeeAsync(employeeName, exeSession, projectDir, opts) {
4910
5585
  const emp = roster.find((e) => baseAgentName(e.name) === baseAgentName(employeeName));
4911
5586
  if (emp && !COORDINATOR_ROLES.has(emp.role)) {
4912
5587
  try {
4913
- const collisions = checkWorktreeCollisions(projectDir);
5588
+ const collisions = await checkWorktreeCollisionsAsync(projectDir);
4914
5589
  if (collisions.length > 0) {
4915
5590
  for (const c of collisions) {
4916
5591
  process.stderr.write(
@@ -4934,11 +5609,36 @@ async function ensureEmployeeAsync(employeeName, exeSession, projectDir, opts) {
4934
5609
  asyncOpts.cwd = wtPath;
4935
5610
  } else {
4936
5611
  const sessionName = employeeSessionName(employeeName, exeSession, asyncOpts.instance);
4937
- const msg = `Worktree isolation failed for code-role ${employeeName}; refusing to spawn in main checkout ${projectDir}`;
4938
- process.stderr.write(`[ensureEmployeeAsync] ${msg}
5612
+ const gate = worktreeIsolationGate(employeeName, projectDir, asyncOpts);
5613
+ if (gate.allow) {
5614
+ asyncOpts.cwd = projectDir;
5615
+ asyncOpts.spawnWorktree = false;
5616
+ const reason = gate.nonGit ? "non_git_project" : "opt_out_override";
5617
+ process.stderr.write(
5618
+ `[ensureEmployeeAsync] WORKTREE BYPASSED: code-role ${employeeName} spawning in ${projectDir} (reason=${reason}) \u2014 no git worktree isolation
5619
+ `
5620
+ );
5621
+ recordOrchestrationEventBestEffort({
5622
+ eventType: "worktree.bypassed",
5623
+ source: "tmux-routing.ensureEmployeeAsync",
5624
+ severity: "warn",
5625
+ agentId: employeeName,
5626
+ sessionScope: exeSession,
5627
+ instanceId: asyncOpts.instance != null ? String(asyncOpts.instance) : null,
5628
+ tmuxSession: sessionName,
5629
+ payload: {
5630
+ agent: employeeName,
5631
+ cwd: projectDir,
5632
+ reason,
5633
+ risk: "code-role agent runs in project root without git worktree isolation"
5634
+ }
5635
+ });
5636
+ } else {
5637
+ process.stderr.write(`[ensureEmployeeAsync] ${gate.message}
4939
5638
  `);
4940
- if (asyncOpts.spawnLockHeld) releaseSpawnLock(sessionName);
4941
- return { status: "failed", sessionName, error: msg };
5639
+ if (asyncOpts.spawnLockHeld) releaseSpawnLock(sessionName);
5640
+ return { status: "failed", sessionName, error: gate.message };
5641
+ }
4942
5642
  }
4943
5643
  }
4944
5644
  } catch (err) {
@@ -4958,11 +5658,21 @@ async function ensureEmployeeAsync(employeeName, exeSession, projectDir, opts) {
4958
5658
  );
4959
5659
  }
4960
5660
  const result = ensureEmployee(employeeName, exeSession, projectDir, asyncOpts);
4961
- if (asyncOpts.spawnLockHeld && result.status === "failed" && result.sessionName) {
5661
+ if (asyncOpts.spawnLockHeld && result.sessionName) {
4962
5662
  releaseSpawnLock(result.sessionName);
4963
5663
  }
4964
5664
  return result;
4965
5665
  }
5666
+ function decideSpawnWorktreeDiscipline(input) {
5667
+ if (input.inWorktree) return "ok";
5668
+ if (input.roleKnown) {
5669
+ if (!input.isCodeRole) return "ok";
5670
+ if (input.optOut) return "bypass";
5671
+ return input.spawnCwdIsGitRepo ? "refuse" : "bypass";
5672
+ }
5673
+ if (input.optOut) return "bypass";
5674
+ return input.spawnCwdIsGitRepo ? "refuse" : "bypass";
5675
+ }
4966
5676
  function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4967
5677
  const transport = getTransport();
4968
5678
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
@@ -5074,32 +5784,50 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5074
5784
  } catch {
5075
5785
  }
5076
5786
  const spawnCwd = opts?.cwd ?? projectDir;
5787
+ let disciplineRefusal = null;
5077
5788
  try {
5078
5789
  const inWorktree = spawnCwd.includes("/.worktrees/") || spawnCwd.includes("/.claude/worktrees/");
5079
- if (!inWorktree) {
5080
- const COORDINATOR_ROLES = /* @__PURE__ */ new Set(["COO", "CTO", "CMO"]);
5081
- const roster = loadEmployeesSync();
5082
- const emp = roster.find(
5083
- (e) => baseAgentName(e.name) === baseAgentName(employeeName)
5084
- );
5085
- const isCodeRole = emp != null && !COORDINATOR_ROLES.has(emp.role);
5086
- if (isCodeRole) {
5087
- const reason = opts?.cwd != null ? "preset_cwd" : "git_timeout";
5088
- process.stderr.write(
5089
- `[tmux-routing] WORKTREE BYPASSED: code-role ${employeeName} (role=${emp.role}) spawning in MAIN checkout ${spawnCwd} (reason=${reason}) \u2014 commits will land on main
5090
- `
5091
- );
5790
+ const COORDINATOR_ROLES = /* @__PURE__ */ new Set(["COO", "CTO", "CMO"]);
5791
+ const rosterResult = loadEmployeesState();
5792
+ const emp = rosterResult.employees.find(
5793
+ (e) => baseAgentName(e.name) === baseAgentName(employeeName)
5794
+ );
5795
+ const roleKnown = emp != null;
5796
+ const isCodeRole = emp != null && !COORDINATOR_ROLES.has(emp.role);
5797
+ const optOut = opts?.spawnWorktree === false;
5798
+ const spawnCwdIsGitRepo = isInsideGitRepo(spawnCwd);
5799
+ const decision = decideSpawnWorktreeDiscipline({
5800
+ inWorktree,
5801
+ roleKnown,
5802
+ isCodeRole,
5803
+ spawnCwdIsGitRepo,
5804
+ optOut
5805
+ });
5806
+ if (decision !== "ok") {
5807
+ const reason = rosterResult.status === "error" && !roleKnown ? "roster_load_error" : opts?.cwd != null ? "preset_cwd" : !roleKnown ? "roster_miss" : "git_timeout";
5808
+ const role = emp?.role ?? "unknown";
5809
+ if (decision === "refuse") {
5810
+ const rosterNote = reason === "roster_load_error" ? ` The roster failed to load (unreadable/corrupt/truncated), so ${employeeName}'s role could not be verified and is treated as code-role.` : reason === "roster_miss" ? ` ${employeeName} is not in the roster, so its role could not be verified and is treated as code-role.` : "";
5811
+ disciplineRefusal = `Worktree isolation required for ${roleKnown ? `code-role ${employeeName} (role=${role})` : employeeName}; refusing to spawn in MAIN checkout ${spawnCwd} (reason=${reason}) \u2014 commits would land on main.${rosterNote} Pass spawn_worktree=false (create_task) or opts.spawnWorktree=false to dispatch here anyway (e.g. a non-git automation project).`;
5812
+ process.stderr.write(`[tmux-routing] ${disciplineRefusal}
5813
+ `);
5092
5814
  recordOrchestrationEventBestEffort({
5093
- eventType: "worktree.bypassed",
5815
+ eventType: "worktree.refused",
5094
5816
  source: "tmux-routing.spawnEmployee",
5095
5817
  severity: "warn",
5096
5818
  agentId: employeeName,
5097
5819
  sessionScope: exeSession,
5098
5820
  instanceId: opts?.instance != null ? String(opts.instance) : null,
5099
5821
  tmuxSession: sessionName,
5100
- payload: { agent: employeeName, role: emp.role, cwd: spawnCwd, reason }
5822
+ payload: { agent: employeeName, role, cwd: spawnCwd, reason, rosterStatus: rosterResult.status }
5101
5823
  });
5102
- } else if (emp == null) {
5824
+ } else {
5825
+ const viaOptOut = optOut;
5826
+ process.stderr.write(
5827
+ `[tmux-routing] WORKTREE BYPASSED: ${role} ${employeeName} spawning in MAIN checkout ${spawnCwd} (reason=${reason}${viaOptOut ? ", opt-out" : ""}) \u2014 commits will land on main
5828
+ `
5829
+ );
5830
+ if (viaOptOut && spawnCwdIsGitRepo) emitOptOutBypassWarning(employeeName, spawnCwd);
5103
5831
  recordOrchestrationEventBestEffort({
5104
5832
  eventType: "worktree.bypassed",
5105
5833
  source: "tmux-routing.spawnEmployee",
@@ -5108,12 +5836,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5108
5836
  sessionScope: exeSession,
5109
5837
  instanceId: opts?.instance != null ? String(opts.instance) : null,
5110
5838
  tmuxSession: sessionName,
5111
- payload: { agent: employeeName, role: "unknown", cwd: spawnCwd, reason: "roster_miss" }
5839
+ payload: { agent: employeeName, role, cwd: spawnCwd, reason, optOut: viaOptOut, rosterStatus: rosterResult.status }
5112
5840
  });
5113
5841
  }
5114
5842
  }
5115
5843
  } catch {
5116
5844
  }
5845
+ if (disciplineRefusal) {
5846
+ releaseSpawnLock(sessionName);
5847
+ return { sessionName, error: disciplineRefusal };
5848
+ }
5117
5849
  const useExeAgent = !!(opts?.model && opts?.provider);
5118
5850
  const agentRtConfig = resolveSpawnRuntimeConfig(
5119
5851
  employeeName,
@@ -5485,13 +6217,6 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5485
6217
  }
5486
6218
 
5487
6219
  // src/lib/task-scope.ts
5488
- var VALID_SESSION_SCOPE_RE = /^[a-zA-Z0-9_]+$/;
5489
- function isValidSessionScope(scope) {
5490
- return typeof scope === "string" && scope.length > 0 && scope.length <= 64 && VALID_SESSION_SCOPE_RE.test(scope);
5491
- }
5492
- function sanitizeSessionScope(scope) {
5493
- return isValidSessionScope(scope) ? scope : null;
5494
- }
5495
6220
  function isAmbiguousMultiCoordinatorContext() {
5496
6221
  if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") return false;
5497
6222
  try {
@@ -5647,7 +6372,7 @@ function sessionScopeFilter(sessionScope, tableAlias, opts) {
5647
6372
  function strictSessionScopeFilter(sessionScope, tableAlias) {
5648
6373
  const autoDetect = sessionScope === void 0;
5649
6374
  const scope = autoDetect ? getCurrentSessionScope() : sessionScope;
5650
- if (!scope) {
6375
+ if (scope == null) {
5651
6376
  if (autoDetect && isAmbiguousMultiCoordinatorContext()) {
5652
6377
  return { sql: " AND 1=0", args: [] };
5653
6378
  }
@@ -5667,6 +6392,7 @@ export {
5667
6392
  claudeSupportsAgentFlag,
5668
6393
  PROVIDER_TABLE,
5669
6394
  DEFAULT_PROVIDER,
6395
+ VALID_SESSION_SCOPE_RE,
5670
6396
  isValidSessionScope,
5671
6397
  sanitizeSessionScope,
5672
6398
  isAmbiguousMultiCoordinatorContext,
@@ -5702,10 +6428,15 @@ export {
5702
6428
  checkLaneAffinity,
5703
6429
  resolveTask,
5704
6430
  createTaskCore,
6431
+ buildTaskRowsStatement,
5705
6432
  queryTaskRows,
5706
6433
  listTasks,
6434
+ __getLiveSessionsTmuxCalls,
6435
+ __resetLiveSessionsCache,
5707
6436
  isTmuxSessionAlive,
5708
6437
  TASK_ALREADY_CLAIMED_PREFIX,
6438
+ TASK_SIDECHAIN_WRITE_BLOCKED_PREFIX,
6439
+ instanceToken,
5709
6440
  VALID_TASK_STATUSES,
5710
6441
  checkStaleCompletion,
5711
6442
  updateTaskStatus,
@@ -5725,6 +6456,7 @@ export {
5725
6456
  STALE_REVIEW_AGE_MS,
5726
6457
  SIGNAL_FILE_TTL_MS,
5727
6458
  isStale,
6459
+ buildPendingReviewsCountStatement,
5728
6460
  countPendingReviews,
5729
6461
  countNewPendingReviewsSince,
5730
6462
  listPendingReviews,
@@ -5750,7 +6482,10 @@ export {
5750
6482
  findFreeInstance,
5751
6483
  verifyPaneAtCapacity,
5752
6484
  resolveSessionRuntime,
6485
+ classifyPaneSessionState,
5753
6486
  getSessionState,
6487
+ getSessionStateAsync,
6488
+ sessionHasPendingInputAsync,
5754
6489
  isSessionBusy,
5755
6490
  isExeSession,
5756
6491
  paneHasPendingInput,
@@ -5758,11 +6493,21 @@ export {
5758
6493
  sessionHasPendingInput,
5759
6494
  decideQueuedNudgeDrain,
5760
6495
  countScopedPendingReviews,
6496
+ countScopedOpenTasks,
6497
+ shouldDispositionWedged,
6498
+ buildScopedInProgressTaskQuery,
6499
+ findScopedInProgressTaskId,
6500
+ offlineIfEmpty,
6501
+ dispositionWedgedSessionTask,
6502
+ decideDrainSend,
5761
6503
  sendIntercom,
5762
6504
  sendIntercomAsync,
5763
6505
  notifyParentExe,
5764
6506
  notifyCoordinatorTaskCompletion,
6507
+ emitOptOutBypassWarning,
6508
+ worktreeIsolationGate,
5765
6509
  ensureEmployee,
5766
6510
  ensureEmployeeAsync,
6511
+ decideSpawnWorktreeDiscipline,
5767
6512
  spawnEmployee
5768
6513
  };