@askexenow/exe-os 0.9.341 → 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 (419) 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-AIQP4DFL.js → active-agent-PVGE57FY.js} +5 -5
  7. package/dist/{active-agent-LW45DSJ4.js → active-agent-TNIO6IY3.js} +5 -5
  8. package/dist/{agentic-ontology-RVUNRCQY.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-ZSDJ4O2T.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-ZAJLEL4X.js → backup-verify-3Y2FHGZU.js} +4 -4
  13. package/dist/{behaviors-EO7KCP3H.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 +25 -23
  24. package/dist/bin/cli.js +79 -25
  25. package/dist/bin/customer-readiness.js +1 -1
  26. package/dist/bin/deferred-daemon-restart.js +98 -92
  27. package/dist/bin/exe-agent-config.js +9 -8
  28. package/dist/bin/exe-agent.js +17 -17
  29. package/dist/bin/exe-assign.js +12 -12
  30. package/dist/bin/exe-boot.js +200 -67
  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 +25 -23
  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 +28 -27
  42. package/dist/bin/exe-kill.js +30 -28
  43. package/dist/bin/exe-launch-agent.js +128 -67
  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 +26 -24
  47. package/dist/bin/exe-pending-notifications.js +25 -23
  48. package/dist/bin/exe-pending-reviews.js +25 -23
  49. package/dist/bin/exe-rename.js +7 -7
  50. package/dist/bin/exe-review.js +29 -27
  51. package/dist/bin/exe-search.js +11 -9
  52. package/dist/bin/exe-session-cleanup.js +115 -85
  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 +26 -24
  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 +26 -24
  62. package/dist/bin/graph-backfill.js +11 -11
  63. package/dist/bin/graph-export.js +9 -9
  64. package/dist/bin/import-history.js +13 -13
  65. package/dist/bin/install.js +103 -97
  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 +2 -2
  73. package/dist/bin/registry-proxy.js +1 -1
  74. package/dist/bin/scan-tasks.js +25 -23
  75. package/dist/bin/setup.js +6 -5
  76. package/dist/bin/shard-migrate.js +9 -9
  77. package/dist/bin/stack-update.js +6 -6
  78. package/dist/bin/update.js +1 -1
  79. package/dist/bin/vps-health-gate.js +1 -1
  80. package/dist/{boot-generation-GSMPJOWM.js → boot-generation-BL2ISK4U.js} +5 -5
  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-OBYQNAKZ.js → capability-cards-DN5UIKVN.js} +4 -4
  86. package/dist/{capacity-monitor-MG3FTUD5.js → capacity-monitor-ZG5GGFR2.js} +26 -24
  87. package/dist/{catchup-brief-BVJOF3GL.js → catchup-brief-D6HH6DWS.js} +31 -28
  88. package/dist/{chunk-Q5AIPSSC.js → chunk-25X4JD2V.js} +10 -6
  89. package/dist/{chunk-6MVI4QH2.js → chunk-2KZNGDSW.js} +1 -1
  90. package/dist/{chunk-WH7XN5ER.js → chunk-2WAVZ5UJ.js} +1 -1
  91. package/dist/{chunk-PTJBWC77.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-X5QVL6RE.js → chunk-4CMVFVFC.js} +173 -69
  95. package/dist/{chunk-2YCFNBC4.js → chunk-4J4PMPXZ.js} +3 -2
  96. package/dist/{chunk-EDOO23B2.js → chunk-4N2NRGCM.js} +2 -2
  97. package/dist/chunk-4QCFFW2H.js +378 -0
  98. package/dist/{chunk-6MVC43X7.js → chunk-4VSV5P4D.js} +2 -2
  99. package/dist/{chunk-HJX6NCJF.js → chunk-4ZCD2UWW.js} +39 -10
  100. package/dist/{chunk-C445WRUX.js → chunk-5ERDIOV3.js} +2 -2
  101. package/dist/chunk-5H47VHB7.js +30 -0
  102. package/dist/{chunk-BJ23BNED.js → chunk-5TY4FPH6.js} +1 -1
  103. package/dist/{chunk-WD55PEBM.js → chunk-6CH3TPHR.js} +3 -3
  104. package/dist/{chunk-TIXTHUVJ.js → chunk-6WQ3NNCE.js} +3 -3
  105. package/dist/{chunk-7LF5DS3A.js → chunk-6XWTQUO4.js} +1 -1
  106. package/dist/chunk-76CJBM36.js +88 -0
  107. package/dist/{chunk-NFBW7N4D.js → chunk-7CTRGNLW.js} +2 -2
  108. package/dist/{chunk-ITMEE7PQ.js → chunk-7V4JADD6.js} +3 -3
  109. package/dist/{chunk-RMOBQ6A5.js → chunk-AD2VCHSS.js} +1 -1
  110. package/dist/{chunk-DNVFWFYF.js → chunk-AJYCFKJR.js} +1 -1
  111. package/dist/chunk-AM7S2TY6.js +54 -0
  112. package/dist/{chunk-RWSLVYTE.js → chunk-AOHXREB7.js} +295 -67
  113. package/dist/{chunk-DGGDUKH6.js → chunk-ASAFVDVR.js} +1 -1
  114. package/dist/{chunk-NTI22F3R.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-BMHQPIJ2.js → chunk-CE7F6RHG.js} +585 -75
  118. package/dist/{chunk-YWQMWFYR.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-YHUNDGVV.js → chunk-EUS4QUKP.js} +1 -1
  123. package/dist/{chunk-6D2PI4QD.js → chunk-EV46QTD3.js} +1 -1
  124. package/dist/{chunk-N7A5D25S.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-MC4ATFJV.js → chunk-FPFPLPUY.js} +6 -6
  128. package/dist/{chunk-UU4UINUX.js → chunk-G4RGKBCA.js} +2 -2
  129. package/dist/{chunk-HAZO7TRD.js → chunk-G5T6KDG4.js} +1 -1
  130. package/dist/{chunk-TWXZM34M.js → chunk-GBTTBXAF.js} +6 -6
  131. package/dist/{chunk-LA2Z6RMS.js → chunk-GDIJE6D7.js} +1 -1
  132. package/dist/{chunk-4QSJVMMT.js → chunk-GG5NDQNN.js} +2 -2
  133. package/dist/{chunk-6BQG7EKO.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-HSYJ6DZ2.js → chunk-H6BV5TKD.js} +39 -24
  137. package/dist/{chunk-HCJCW7X4.js → chunk-H7I47RS5.js} +34 -1
  138. package/dist/{chunk-GFDGDFBF.js → chunk-HA5R6USD.js} +22 -5
  139. package/dist/{chunk-L2YGXJGF.js → chunk-HAF2DJYV.js} +3 -3
  140. package/dist/chunk-HJ447YER.js +224 -0
  141. package/dist/{chunk-6QW6JHRE.js → chunk-HV4KESRY.js} +59 -32
  142. package/dist/{chunk-BJOBR6P2.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-2UGGBIDL.js → chunk-JGIPOUIK.js} +3 -3
  146. package/dist/{chunk-LLU6WZLK.js → chunk-JLZ4RYSD.js} +2 -2
  147. package/dist/{chunk-WWNQFOSP.js → chunk-JMYHW3AL.js} +2 -2
  148. package/dist/{chunk-KP4PY2Z2.js → chunk-JPUZPAAF.js} +4 -4
  149. package/dist/{chunk-4LWV633D.js → chunk-JTBIS4R5.js} +1 -1
  150. package/dist/{chunk-PPZOWQHP.js → chunk-K7KFGDWF.js} +1 -1
  151. package/dist/{chunk-HNLXMD2V.js → chunk-KGWRTRKC.js} +2 -2
  152. package/dist/{chunk-IB336P52.js → chunk-KP3U5QGJ.js} +1 -1
  153. package/dist/{chunk-OUEFW4RK.js → chunk-KW2SD2FL.js} +2 -2
  154. package/dist/{chunk-2P4WJNN5.js → chunk-KWZMQACA.js} +11 -7
  155. package/dist/{chunk-ATKW3SYL.js → chunk-L2R3LN2P.js} +1 -1
  156. package/dist/{chunk-IELUDZAA.js → chunk-L4A5DL6Z.js} +3 -3
  157. package/dist/{chunk-2C455MU3.js → chunk-LU2FALL6.js} +3 -3
  158. package/dist/{chunk-LOBCQXV5.js → chunk-MKYFAKDW.js} +4 -4
  159. package/dist/{chunk-3BRVLPPA.js → chunk-MOPUCPXK.js} +4 -0
  160. package/dist/{chunk-BR4423AS.js → chunk-MR6LHP4F.js} +1 -1
  161. package/dist/chunk-MVYB76QG.js +341 -0
  162. package/dist/{chunk-ZNE6H2JO.js → chunk-MZM6EJCU.js} +4 -4
  163. package/dist/{chunk-FOV57K47.js → chunk-N7QJKQMX.js} +258 -14
  164. package/dist/{chunk-AYLNJGCS.js → chunk-NBZL5CGA.js} +93 -12
  165. package/dist/{chunk-YLQXP7T7.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-BV6GBSRP.js → chunk-OLMQ4UOF.js} +42 -10
  171. package/dist/{chunk-UIU2DGRV.js → chunk-OTF44GXL.js} +4 -4
  172. package/dist/{chunk-GOGSWXNZ.js → chunk-OZEBAGKR.js} +277 -137
  173. package/dist/{chunk-NDSNM5OH.js → chunk-P5HIWY2E.js} +1 -1
  174. package/dist/{chunk-MJB5ZFQM.js → chunk-PDWS3E22.js} +55 -4
  175. package/dist/{chunk-4FJZ6SO7.js → chunk-PPCDBE6Z.js} +1 -1
  176. package/dist/{chunk-7SEC2AG5.js → chunk-PQIDGXSG.js} +13 -4
  177. package/dist/chunk-PWUFMX75.js +186 -0
  178. package/dist/{chunk-GPW32HVB.js → chunk-QDGZJ653.js} +3 -3
  179. package/dist/{chunk-CYA5S5OD.js → chunk-QOO5RW3K.js} +6 -5
  180. package/dist/{chunk-IXBIVKWZ.js → chunk-QQ4E2RYW.js} +95 -18
  181. package/dist/{chunk-2WMTTIAZ.js → chunk-QRYVCDWS.js} +2 -2
  182. package/dist/{chunk-Z42MXYWW.js → chunk-QWVXN224.js} +110 -28
  183. package/dist/{chunk-XSMRR4IW.js → chunk-QYZJHTNE.js} +26 -27
  184. package/dist/{chunk-I7ECEP3W.js → chunk-R2NN3JKH.js} +43 -6
  185. package/dist/{chunk-B7CUBLF7.js → chunk-RACAOE2Z.js} +13 -13
  186. package/dist/{chunk-5RACW3LX.js → chunk-RAXVPNMR.js} +1 -1
  187. package/dist/{chunk-CMR64RYM.js → chunk-RHWZUUS4.js} +17 -17
  188. package/dist/{chunk-RNAYCUNM.js → chunk-S3THSOWV.js} +1 -1
  189. package/dist/{chunk-U2MREVXQ.js → chunk-S7QUM2UK.js} +2 -2
  190. package/dist/{chunk-SSTLTIF3.js → chunk-SKGMF2SO.js} +25 -2
  191. package/dist/{chunk-J6SEDDOR.js → chunk-SQKXKV2Y.js} +2 -2
  192. package/dist/{chunk-DPFQ2WQX.js → chunk-TLT7NZCW.js} +4 -4
  193. package/dist/chunk-TTCUG2CF.js +91 -0
  194. package/dist/{chunk-W6K4XB6G.js → chunk-UEA3LXOB.js} +1 -1
  195. package/dist/{chunk-WNKVKNOM.js → chunk-UNXQBELK.js} +1 -1
  196. package/dist/{chunk-KKOLHBNC.js → chunk-UP75Z5X7.js} +904 -167
  197. package/dist/{chunk-Y3S4H77G.js → chunk-USSB56WK.js} +1 -1
  198. package/dist/{chunk-PVIH3ZGV.js → chunk-UUEKJDK6.js} +90 -10
  199. package/dist/{chunk-T2PYVVZ5.js → chunk-UUQBZPJJ.js} +1 -1
  200. package/dist/{chunk-32YHCHWW.js → chunk-V3EEVY4L.js} +1 -1
  201. package/dist/{chunk-2IWBY7XK.js → chunk-VPZHJCKW.js} +2 -2
  202. package/dist/chunk-VRAQTFD6.js +265 -0
  203. package/dist/{chunk-EDH54E5A.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-CCIEIRV2.js → chunk-X33NYQYJ.js} +2 -2
  209. package/dist/{chunk-K6KRBXQP.js → chunk-XHK3FS3W.js} +3 -2
  210. package/dist/{chunk-AP455LBW.js → chunk-XZXBB3TN.js} +2 -2
  211. package/dist/{chunk-JSKBHUR7.js → chunk-YCANDMI2.js} +2 -2
  212. package/dist/{chunk-YIIME2OW.js → chunk-YLXTKI6M.js} +1 -1
  213. package/dist/{chunk-LEJ25726.js → chunk-YTCFACP4.js} +650 -434
  214. package/dist/{chunk-HUSVERE6.js → chunk-Z6RSN7KS.js} +7 -6
  215. package/dist/{cleanup-graph-garbage-MXQ4Q2MA.js → cleanup-graph-garbage-XWUFAMFO.js} +8 -8
  216. package/dist/{co-activation-QI5BK4RT.js → co-activation-ZCHPLLP6.js} +4 -4
  217. package/dist/{co-occurrence-BYSQNZSV.js → co-occurrence-KG3LKALN.js} +6 -6
  218. package/dist/{code-context-index-BNBHDXHW.js → code-context-index-WSOJQ72R.js} +5 -5
  219. package/dist/{content-extractor-JRJDNIHP.js → content-extractor-NXYNEF5K.js} +2 -2
  220. package/dist/{conversation-entity-extractor-FPAMTL3C.js → conversation-entity-extractor-D3EFTLT4.js} +2 -2
  221. package/dist/{conversation-wiki-populator-WU4HEQ7C.js → conversation-wiki-populator-WQN3IUU6.js} +1 -1
  222. package/dist/{crdt-sync-ETPKNBCU.js → crdt-sync-4PFGPZXV.js} +8 -1
  223. package/dist/{crm-webhook-M566ZMZS.js → crm-webhook-II7Z45Q6.js} +2 -2
  224. package/dist/cto-delegation-gate-JWWOJPNC.js +669 -0
  225. package/dist/{daemon-auth-7IFSV5FG.js → daemon-auth-3FEAHFWC.js} +5 -3
  226. package/dist/{daemon-orchestration-XV5GN73R.js → daemon-orchestration-FX4XZDR5.js} +45 -26
  227. package/dist/{daemon-ready-RKKB4P6K.js → daemon-ready-4JTSFKAY.js} +1 -1
  228. package/dist/{daemon-ready-signal-WKEJQNRA.js → daemon-ready-signal-LPAZWQOV.js} +3 -1
  229. package/dist/daemon-restart-circuit-breaker-PEGEIP6H.js +35 -0
  230. package/dist/daemon-timer-report-NM44WQRH.js +25 -0
  231. package/dist/{db-backup-MX2G3CEK.js → db-backup-W2QFSYQC.js} +15 -5
  232. package/dist/{db-restore-events-JVEEP52Z.js → db-restore-events-MQ2DBGEQ.js} +2 -2
  233. package/dist/dispatch-ack-VYH3ONSS.js +27 -0
  234. package/dist/{doc-graph-extractor-LR5KMPH7.js → doc-graph-extractor-F6ODFIID.js} +6 -6
  235. package/dist/{documents-FA6N7QDA.js → documents-23WX2FPQ.js} +12 -12
  236. package/dist/dreaming-NWD22UQ5.js +46 -0
  237. package/dist/{exe-drift-TUKTCXZE.js → exe-drift-CW72FBGE.js} +5 -5
  238. package/dist/{exe-export-ZLHBXMZC.js → exe-export-AUJ7CV7G.js} +11 -11
  239. package/dist/{exe-import-VVKBZWSA.js → exe-import-JX4I52VE.js} +11 -11
  240. package/dist/{exe-key-QJXFBF6G.js → exe-key-2N6LY2BU.js} +242 -228
  241. package/dist/{exe-org-BGZNNE4H.js → exe-org-X4CSNCKS.js} +3 -3
  242. package/dist/{exe-snapshot-JRLUO645.js → exe-snapshot-XZ7FVCYZ.js} +30 -28
  243. package/dist/{fast-db-init-JKJEEHUI.js → fast-db-init-4CWHSOOR.js} +1 -1
  244. package/dist/{founder-context-HWPLMZVR.js → founder-context-7L3CFPYN.js} +4 -4
  245. package/dist/gateway/index.js +25 -25
  246. package/dist/{gateway-client-GRBOJQYC.js → gateway-client-7LMZKHTH.js} +1 -1
  247. package/dist/git-main-guard-UZP3KXYI.js +39 -0
  248. package/dist/{git-staleness-X5XZONX3.js → git-staleness-CI2R4QOJ.js} +4 -4
  249. package/dist/{global-procedures-VIIVWB6R.js → global-procedures-XQX3LUH6.js} +6 -6
  250. package/dist/{graph-auto-extract-HCQQF5AG.js → graph-auto-extract-OURWMGBS.js} +6 -6
  251. package/dist/{graph-rag-WAK2BRJH.js → graph-rag-JZZAB7PC.js} +2 -2
  252. package/dist/{hook-integrity-WURH6DS3.js → hook-integrity-XY47IM7G.js} +1 -1
  253. package/dist/hook-prefetch-PHRRKY4C.js +137 -0
  254. package/dist/hooks/bug-report-worker.js +31 -27
  255. package/dist/hooks/codex-stop-task-finalizer.js +31 -27
  256. package/dist/hooks/commit-complete.js +28 -26
  257. package/dist/hooks/error-recall.js +12 -11
  258. package/dist/hooks/exe-heartbeat-hook.js +6 -6
  259. package/dist/hooks/ingest-worker.js +11 -11
  260. package/dist/hooks/ingest.js +18 -18
  261. package/dist/hooks/instructions-loaded.js +7 -7
  262. package/dist/hooks/manifest.json +20 -20
  263. package/dist/hooks/notification.js +7 -7
  264. package/dist/hooks/post-compact.js +27 -25
  265. package/dist/hooks/post-tool-combined.js +12 -12
  266. package/dist/hooks/pre-compact.js +28 -26
  267. package/dist/hooks/pre-tool-use.js +179 -46
  268. package/dist/hooks/prompt-submit.js +323 -272
  269. package/dist/hooks/session-end.js +34 -32
  270. package/dist/hooks/session-start.js +28 -21
  271. package/dist/hooks/stop.js +34 -32
  272. package/dist/hooks/subagent-stop.js +27 -25
  273. package/dist/hooks/summary-worker.js +35 -33
  274. package/dist/index.js +40 -38
  275. package/dist/{installer-6IFVEQI4.js → installer-G3JAO3NT.js} +9 -9
  276. package/dist/{installer-NX2CLNV5.js → installer-JND3RJ6Q.js} +9 -9
  277. package/dist/{installer-T2LGHZKL.js → installer-L7EVH5U5.js} +9 -9
  278. package/dist/{intercom-queue-KYHXXZCQ.js → intercom-queue-ZFEQALR3.js} +24 -22
  279. package/dist/{key-backup-status-TNEHXA2X.js → key-backup-status-LOSMVATJ.js} +2 -2
  280. package/dist/lib/agent-config.js +4 -3
  281. package/dist/lib/cloud-sync.js +19 -19
  282. package/dist/lib/config.js +2 -2
  283. package/dist/lib/consolidation.js +11 -11
  284. package/dist/lib/crypto.js +3 -3
  285. package/dist/lib/database.js +6 -4
  286. package/dist/lib/db-daemon-client.js +5 -5
  287. package/dist/lib/db-read-worker-client.js +9 -3
  288. package/dist/lib/db.js +6 -4
  289. package/dist/lib/device-registry.js +3 -3
  290. package/dist/lib/embed-worker.js +2 -2
  291. package/dist/lib/embedder.js +5 -5
  292. package/dist/lib/employee-templates.js +7 -7
  293. package/dist/lib/employees.js +10 -4
  294. package/dist/lib/exe-daemon-client.js +4 -4
  295. package/dist/lib/exe-daemon.js +588 -164
  296. package/dist/lib/hybrid-search.js +11 -9
  297. package/dist/lib/identity.js +4 -4
  298. package/dist/lib/keychain.js +1 -1
  299. package/dist/lib/license.js +4 -4
  300. package/dist/lib/messaging.js +27 -23
  301. package/dist/lib/post-tool-memory.js +3 -3
  302. package/dist/lib/registry-proxy.js +1 -1
  303. package/dist/lib/reminders.js +15 -7
  304. package/dist/lib/schedules.js +9 -9
  305. package/dist/lib/session-registry.js +6 -6
  306. package/dist/lib/skill-learning.js +10 -10
  307. package/dist/lib/status-brief.js +4 -4
  308. package/dist/lib/store.js +8 -8
  309. package/dist/lib/task-router.js +5 -5
  310. package/dist/lib/tasks.js +28 -24
  311. package/dist/lib/tmux-routing.js +55 -24
  312. package/dist/lib/tmux-status.js +9 -3
  313. package/dist/lib/tmux-transport.js +1 -1
  314. package/dist/lib/token-spend.js +5 -5
  315. package/dist/lib/transport.js +2 -2
  316. package/dist/lib/ws-client.js +1 -1
  317. package/dist/{license-gate-MGDR4YNI.js → license-gate-QTRQHV4F.js} +5 -5
  318. package/dist/{license-sync-UWE6GEB4.js → license-sync-N3UJFWAW.js} +4 -4
  319. package/dist/mcp/register-tools.js +97 -91
  320. package/dist/mcp/server.js +99 -93
  321. package/dist/mcp/tools/complete-reminder.js +6 -6
  322. package/dist/mcp/tools/create-reminder.js +6 -6
  323. package/dist/mcp/tools/create-task.js +36 -27
  324. package/dist/mcp/tools/deactivate-behavior.js +11 -11
  325. package/dist/mcp/tools/list-reminders.js +6 -6
  326. package/dist/mcp/tools/list-tasks.js +34 -28
  327. package/dist/mcp/tools/send-message.js +27 -25
  328. package/dist/mcp/tools/update-task.js +33 -27
  329. package/dist/{mcp-http-config-RQLYEPTL.js → mcp-http-config-BDL36JZ3.js} +6 -6
  330. package/dist/{mcp-port-config-FFAMMLLE.js → mcp-port-config-6HYY6J7K.js} +2 -2
  331. package/dist/mcp-session-identity-T7H2EKFQ.js +177 -0
  332. package/dist/{memory-cards-VYQ5DPMR.js → memory-cards-ZRDW7QBX.js} +8 -8
  333. package/dist/{memory-graph-extractor-GX2HNYLU.js → memory-graph-extractor-QLXDMHRS.js} +7 -7
  334. package/dist/{memory-poisoning-defense-TX7627BD.js → memory-poisoning-defense-HJTXYCWH.js} +4 -4
  335. package/dist/{memory-pressure-watchdog-MLNNH26B.js → memory-pressure-watchdog-HN5X7AVN.js} +75 -36
  336. package/dist/{memory-queue-X2CSEAKE.js → memory-queue-2UOOF27L.js} +5 -3
  337. package/dist/{memory-queue-client-YRL6XTGU.js → memory-queue-client-O5U6F6XP.js} +6 -6
  338. package/dist/{memory-reflection-KIGEFSI4.js → memory-reflection-WVNFBJAA.js} +4 -4
  339. package/dist/{monitor-grants-RZOVSYVO.js → monitor-grants-E3ELDEWD.js} +5 -5
  340. package/dist/{monitor-identity-MOXISYNG.js → monitor-identity-GLFZSEAH.js} +2 -2
  341. package/dist/{notification-ledger-B6AVEGMV.js → notification-ledger-SZEPGJQ7.js} +25 -23
  342. package/dist/{notifications-DWXV7SVL.js → notifications-4UT53CDG.js} +24 -22
  343. package/dist/{orchestration-events-ZG7O4HRY.js → orchestration-events-WV4NWKZH.js} +5 -5
  344. package/dist/{orchestration-phase-CXRCBCK7.js → orchestration-phase-AWBXPO52.js} +3 -3
  345. package/dist/orchestrator-UAC3RW2T.js +47 -0
  346. package/dist/{pipeline-router-RTLAGZTA.js → pipeline-router-LM53NMWJ.js} +5 -5
  347. package/dist/{plan-limits-WLQZTCSF.js → plan-limits-2FEY2GOS.js} +7 -7
  348. package/dist/platform-procedures-X5DFJFV7.js +13 -0
  349. package/dist/{preferences-TYTGDXQZ.js → preferences-TWIXZBKS.js} +2 -2
  350. package/dist/{project-boot-6CGQ3EAH.js → project-boot-4NUH5FRU.js} +2 -2
  351. package/dist/{projection-worker-44SEKTZ5.js → projection-worker-FZ7YSN3G.js} +5 -5
  352. package/dist/{prospective-memory-UACFTYXK.js → prospective-memory-6D2ASWM3.js} +4 -4
  353. package/dist/{push-notifications-W5JRXLKJ.js → push-notifications-U6FIBQRW.js} +3 -3
  354. package/dist/{reranker-NL4YAV6R.js → reranker-GYYRPANK.js} +3 -3
  355. package/dist/{retrieval-health-4C2PO6HH.js → retrieval-health-4Z242P3B.js} +1 -1
  356. package/dist/{review-polling-W5AMHNLT.js → review-polling-HXAFJZDA.js} +25 -23
  357. package/dist/runtime/index.js +35 -33
  358. package/dist/{session-events-PGL5GJEB.js → session-events-O3BV56ZX.js} +25 -23
  359. package/dist/{session-kill-telemetry-6YDGEF27.js → session-kill-telemetry-AELE4JDK.js} +25 -23
  360. package/dist/session-mcp-config-YKNXG5DW.js +50 -0
  361. package/dist/{session-scope-3MEOO557.js → session-scope-5EAPUT45.js} +24 -22
  362. package/dist/setup-wizard-NB6R4QVS.js +14 -0
  363. package/dist/{shard-manager-MBWQEYNG.js → shard-manager-35PWFR3B.js} +3 -3
  364. package/dist/{skill-refinement-EOROTBPX.js → skill-refinement-DVSYXBBP.js} +5 -5
  365. package/dist/{stack-release-PR2XKJ4Q.js → stack-release-HGFFFCGC.js} +90 -15
  366. package/dist/{stack-update-LMDM27ZW.js → stack-update-ATCJFOEK.js} +5 -5
  367. package/dist/{steward-gate-4A2XHYFD.js → steward-gate-3GQ5XKMJ.js} +5 -5
  368. package/dist/{support-inbox-Z77ACQUY.js → support-inbox-4L62P4EP.js} +7 -6
  369. package/dist/{support-outbox-GB5SQFRJ.js → support-outbox-WHGPUYL2.js} +2 -2
  370. package/dist/task-enforcement-IPIZX2MM.js +90 -0
  371. package/dist/{task-scope-EKYDCVUF.js → task-scope-FOIW25HR.js} +26 -22
  372. package/dist/{tasks-crud-6LBNU43G.js → tasks-crud-HBDKE6C5.js} +34 -22
  373. package/dist/tasks-notify-G2U2U2YA.js +53 -0
  374. package/dist/{tasks-review-PBIUYXAL.js → tasks-review-27SFX5XN.js} +26 -22
  375. package/dist/{telemetry-upload-WQDFABTF.js → telemetry-upload-ZOXHRWR5.js} +9 -9
  376. package/dist/tmux-command-guard-KSJKIX3C.js +117 -0
  377. package/dist/{token-budget-KWGL5DY7.js → token-budget-5Y4YPDO3.js} +5 -5
  378. package/dist/{tool-capability-index-EH3HQVHU.js → tool-capability-index-VYF5GNJ2.js} +1 -1
  379. package/dist/{tool-telemetry-UC55LSIH.js → tool-telemetry-KBXSBDTK.js} +1 -1
  380. package/dist/tui/App.js +39 -37
  381. package/dist/{tui-data-M3RBSJL3.js → tui-data-NLFMBIQI.js} +24 -22
  382. package/dist/{update-check-HFO4VHO4.js → update-check-3FHAWIG2.js} +7 -3
  383. package/dist/{update-support-VPBD5SD2.js → update-support-UVUSZG7R.js} +93 -93
  384. package/dist/{whatsapp-accounts-BWRZUY23.js → whatsapp-accounts-BFIOXYW5.js} +1 -1
  385. package/dist/{whatsapp-config-GOSELKTE.js → whatsapp-config-FAX6EA5K.js} +20 -13
  386. package/dist/{worker-gate-4QLLMXZM.js → worker-gate-3KUBAU32.js} +3 -3
  387. package/dist/{workflow-engine-MMEHKUN4.js → workflow-engine-YITHXZR5.js} +2 -2
  388. package/dist/{working-memory-PY4AG577.js → working-memory-NUVB5SFV.js} +1 -1
  389. package/dist/worktree-LL62PLQB.js +47 -0
  390. package/dist/{worktree-sweep-5WKBU7E6.js → worktree-sweep-WTK3MTKH.js} +7 -7
  391. package/package.json +3 -2
  392. package/release-notes.json +75 -43
  393. package/dist/branch-hygiene-R5NVTRAY.js +0 -138
  394. package/dist/chunk-7YORGTXC.js +0 -138
  395. package/dist/chunk-CCPHEPLU.js +0 -30
  396. package/dist/chunk-HMP5VT6A.js +0 -118
  397. package/dist/chunk-PVYR4FCQ.js +0 -113
  398. package/dist/chunk-RP5GETX5.js +0 -140
  399. package/dist/chunk-YGAAZN3E.js +0 -47
  400. package/dist/cto-delegation-gate-S734XDXT.js +0 -290
  401. package/dist/dispatch-ack-4S44HBWB.js +0 -15
  402. package/dist/dreaming-UKGTQT6I.js +0 -44
  403. package/dist/orchestrator-PGHJDUUT.js +0 -45
  404. package/dist/setup-wizard-CJIYDSSP.js +0 -13
  405. package/dist/tasks-notify-TFJLSQ3B.js +0 -50
  406. package/dist/worktree-YXBDRYOC.js +0 -31
  407. package/dist/{chunk-7UMCQQWS.js → chunk-A4DVMPYB.js} +3 -3
  408. /package/dist/{chunk-2KLAYDFB.js → chunk-GIBYNT6W.js} +0 -0
  409. /package/dist/{chunk-MC6VPZKS.js → chunk-JRM4TAGS.js} +0 -0
  410. /package/dist/{chunk-3NCZKWPO.js → chunk-MGGOSFCR.js} +0 -0
  411. /package/dist/{chunk-C2SRRCRK.js → chunk-QIFYLIUF.js} +0 -0
  412. /package/dist/{chunk-OADURIIN.js → chunk-WTH4CSAP.js} +0 -0
  413. /package/dist/{core-memory-3IBWM4WR.js → core-memory-GAC6EWOQ.js} +0 -0
  414. /package/dist/{entity-boost-DWAWXFKN.js → entity-boost-NT6UO2Z3.js} +0 -0
  415. /package/dist/{message-queue-client-3PTHPG5Q.js → message-queue-client-4EOQRUXW.js} +0 -0
  416. /package/dist/{oauth-server-M5TGXT6D.js → oauth-server-67YBDP32.js} +0 -0
  417. /package/dist/{webhook-pipe-FZYLO6RR.js → webhook-pipe-LEXMUQ5B.js} +0 -0
  418. /package/dist/{wiki-acl-7CROE3MJ.js → wiki-acl-74BLWG6J.js} +0 -0
  419. /package/dist/{wiki-client-VNDOBK63.js → wiki-client-EAN7F5LV.js} +0 -0
@@ -1,16 +1,19 @@
1
1
  import {
2
2
  ensureAgentSymlink
3
- } from "./chunk-ATKW3SYL.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-BJOBR6P2.js";
10
+ } from "./chunk-I26FWCSP.js";
9
11
  import {
10
12
  incrementSkillSuccess
11
- } from "./chunk-6D2PI4QD.js";
13
+ } from "./chunk-EV46QTD3.js";
12
14
  import {
13
15
  LEASE_MINUTES,
16
+ isLeaseExpired,
14
17
  leaseExpiryIso
15
18
  } from "./chunk-E23LNUWJ.js";
16
19
  import {
@@ -24,35 +27,35 @@ import {
24
27
  import {
25
28
  listSessions,
26
29
  registerSession
27
- } from "./chunk-HNLXMD2V.js";
30
+ } from "./chunk-KGWRTRKC.js";
28
31
  import {
29
32
  getTransport
30
- } from "./chunk-RMOBQ6A5.js";
33
+ } from "./chunk-AD2VCHSS.js";
31
34
  import {
32
35
  listTmuxSessions
33
- } from "./chunk-SSTLTIF3.js";
36
+ } from "./chunk-SKGMF2SO.js";
34
37
  import {
35
38
  recordOrchestrationEventBestEffort
36
- } from "./chunk-WH7XN5ER.js";
39
+ } from "./chunk-2WAVZ5UJ.js";
37
40
  import {
38
41
  assertRuntimeModelCompatible,
39
42
  getAgentRuntime,
40
43
  normalizeCcModelName,
41
44
  resolveSpawnRuntimeConfig
42
- } from "./chunk-GFDGDFBF.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-2UGGBIDL.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-LLU6WZLK.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-RWSLVYTE.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-7SEC2AG5.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-3MEOO557.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-HWPLMZVR.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",
2302
+ taskId,
2303
+ agentId: eventAgentId,
2304
+ sessionScope: eventSessionScope,
2305
+ projectName: eventProjectName,
2306
+ tmuxSession,
2307
+ result: "assigner_override",
2308
+ payload: { callerAgentId: input.callerAgentId }
2309
+ });
2310
+ recordOrchestrationEventBestEffort({
2311
+ eventType: "task.in_progress",
2312
+ source: "tasks-crud.updateTaskStatus",
1981
2313
  taskId,
1982
- step: "assigner_override",
1983
- contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
2314
+ agentId: eventAgentId,
2315
+ sessionScope: eventSessionScope,
2316
+ projectName: eventProjectName,
2317
+ tmuxSession,
2318
+ result: "assigner_override"
1984
2319
  });
1985
- } catch {
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() {
@@ -3746,7 +4206,7 @@ async function exportBehaviorsAsync(agentId, projectName, sessionKey) {
3746
4206
  const script = resolveBehaviorsExporterScript();
3747
4207
  if (!script) return null;
3748
4208
  try {
3749
- const { stdout } = await execFileAsync(
4209
+ const { stdout } = await execFileAsync3(
3750
4210
  process.execPath,
3751
4211
  [script, agentId, projectName, sessionKey],
3752
4212
  { encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
@@ -3827,6 +4287,13 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3827
4287
  mkdirSync6(SESSION_CACHE, { recursive: true });
3828
4288
  }
3829
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
+ }
3830
4297
  const filePath = path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3831
4298
  atomicWriteJsonSync(filePath, {
3832
4299
  parentExe: rootExe,
@@ -3861,14 +4328,28 @@ function getDispatchedBy(sessionKey) {
3861
4328
  function resolveExeSession() {
3862
4329
  const mySession = getMySession();
3863
4330
  const fromSessionName = mySession ? extractRootExe(mySession) ?? null : null;
3864
- const alsHint = (() => {
4331
+ const agentCtx = (() => {
3865
4332
  try {
3866
- return getAgentContext()?.sessionHint ?? "";
4333
+ return getAgentContext();
3867
4334
  } catch {
3868
- return "";
4335
+ return void 0;
3869
4336
  }
3870
4337
  })();
3871
- 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 || "";
3872
4353
  const validateLiveHint = (hint, source) => {
3873
4354
  if (!hint || !isRootSession(hint)) return hint || null;
3874
4355
  try {
@@ -3898,7 +4379,17 @@ function resolveExeSession() {
3898
4379
  return hint;
3899
4380
  }
3900
4381
  };
3901
- 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
+ }
3902
4393
  if (sessionHintRaw) {
3903
4394
  const fromEnv = extractRootExe(sessionHintRaw) ?? sessionHintRaw;
3904
4395
  if (fromEnv) {
@@ -4101,7 +4592,7 @@ async function verifyPaneAtCapacity(sessionName) {
4101
4592
  reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
4102
4593
  };
4103
4594
  }
4104
- const { isAtCapacity } = await import("./capacity-monitor-MG3FTUD5.js");
4595
+ const { isAtCapacity } = await import("./capacity-monitor-ZG5GGFR2.js");
4105
4596
  if (!isAtCapacity(pane)) {
4106
4597
  return {
4107
4598
  atCapacity: false,
@@ -4117,7 +4608,7 @@ var INTERCOM_DEBOUNCE_MS = 3e5;
4117
4608
  var CODEX_DEBOUNCE_MS = 3e5;
4118
4609
  var INTERCOM_LOG2 = path8.join(os6.homedir(), ".exe-os", "intercom.log");
4119
4610
  var INTERCOM_LOG_MAX_BYTES2 = 1024 * 1024;
4120
- var execFileAsync = promisify(execFile);
4611
+ var execFileAsync3 = promisify3(execFile2);
4121
4612
  var DEBOUNCE_FILE = path8.join(SESSION_CACHE, "intercom-debounce.json");
4122
4613
  var DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4123
4614
  function readDebounceState() {
@@ -4196,24 +4687,47 @@ function logIntercom(msg, meta) {
4196
4687
  } catch {
4197
4688
  }
4198
4689
  }
4199
- 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
+ }
4200
4700
  function getSessionState(sessionName) {
4201
4701
  const transport = getTransport();
4202
4702
  if (!transport.isAlive(sessionName)) return "offline";
4203
4703
  try {
4204
- const pane = transport.capturePane(sessionName, 5);
4205
- if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4206
- if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4207
- return "no_claude";
4208
- }
4209
- }
4210
- if (/Running…/.test(pane)) return "tool";
4211
- if (BUSY_PATTERN.test(pane)) return "thinking";
4212
- 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));
4213
4720
  } catch {
4214
4721
  return "offline";
4215
4722
  }
4216
4723
  }
4724
+ async function sessionHasPendingInputAsync(sessionName) {
4725
+ try {
4726
+ return paneHasPendingInput(await captureSessionPaneAsync(sessionName, 12));
4727
+ } catch {
4728
+ return false;
4729
+ }
4730
+ }
4217
4731
  function isSessionBusy(sessionName) {
4218
4732
  const state = getSessionState(sessionName);
4219
4733
  return state === "thinking" || state === "tool";
@@ -4256,7 +4770,7 @@ function sessionHasPendingInput(sessionName) {
4256
4770
  }
4257
4771
  }
4258
4772
  function decideQueuedNudgeDrain(intercomReason, state) {
4259
- if (state.hasInProgressTask) {
4773
+ if (state.hasInProgressTask && state.isIdle === false) {
4260
4774
  return { action: "drop", reason: "in_progress_task_will_auto_chain" };
4261
4775
  }
4262
4776
  if (intercomReason === "message" && !state.hasUnreadMessages) {
@@ -4279,6 +4793,111 @@ async function countScopedPendingReviews(targetSession, reviewer) {
4279
4793
  return null;
4280
4794
  }
4281
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
+ }
4282
4901
  function sendIntercom(targetSession, opts) {
4283
4902
  const transport = getTransport();
4284
4903
  const startedAt = Date.now();
@@ -4351,7 +4970,7 @@ function sendIntercom(targetSession, opts) {
4351
4970
  }
4352
4971
  if ((process.env.EXE_NOTIFICATION_LEDGER ?? "").trim().toLowerCase() === "shadow") {
4353
4972
  const shadowScope = opts && "callerSessionScope" in opts ? opts.callerSessionScope : void 0;
4354
- void import("./notification-ledger-B6AVEGMV.js").then((m) => m.recordShadowNudge(targetSession, reason, shadowScope)).catch(() => {
4973
+ void import("./notification-ledger-SZEPGJQ7.js").then((m) => m.recordShadowNudge(targetSession, reason, shadowScope)).catch(() => {
4355
4974
  });
4356
4975
  }
4357
4976
  if (!opts?.force && isDebounced(targetSession)) {
@@ -4472,7 +5091,7 @@ function scheduleDeferredSpawnNudge(input) {
4472
5091
  return;
4473
5092
  }
4474
5093
  queueIntercom(input.sessionName, `spawn nudge failed after ${attempt} attempts: ${msg}`, "signal");
4475
- void import("./notifications-DWXV7SVL.js").then(({ writeNotification: writeNotification2 }) => writeNotification2({
5094
+ void import("./notifications-4UT53CDG.js").then(({ writeNotification: writeNotification2 }) => writeNotification2({
4476
5095
  agentId: getCoordinatorName(),
4477
5096
  agentRole: "COO",
4478
5097
  event: "error_spike",
@@ -4492,7 +5111,7 @@ async function sendKeysLiteralAsync(targetSession, text, runtime) {
4492
5111
  const isCodex = runtime === "codex";
4493
5112
  const submit = async () => {
4494
5113
  await new Promise((r) => setTimeout(r, isCodex ? CODEX_SUBMIT_DELAY_MS : SUBMIT_DELAY_MS));
4495
- 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 });
4496
5115
  };
4497
5116
  if (text.length >= 500) {
4498
5117
  const tmpDir = path8.join(os6.tmpdir(), "exe-os-intercom");
@@ -4500,8 +5119,8 @@ async function sendKeysLiteralAsync(targetSession, text, runtime) {
4500
5119
  const tmpFile = path8.join(tmpDir, `msg-${process.pid}-${Date.now()}.txt`);
4501
5120
  try {
4502
5121
  writeFileSync5(tmpFile, text, "utf-8");
4503
- await execFileAsync("tmux", ["load-buffer", tmpFile], { timeout: 3e3 });
4504
- 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 });
4505
5124
  await submit();
4506
5125
  } finally {
4507
5126
  try {
@@ -4511,7 +5130,7 @@ async function sendKeysLiteralAsync(targetSession, text, runtime) {
4511
5130
  }
4512
5131
  return;
4513
5132
  }
4514
- await execFileAsync("tmux", ["send-keys", "-t", targetSession, "-l", text], { timeout: 3e3 });
5133
+ await execFileAsync3("tmux", ["send-keys", "-t", targetSession, "-l", text], { timeout: 3e3 });
4515
5134
  await submit();
4516
5135
  }
4517
5136
  async function sendIntercomAsync(targetSession, opts) {
@@ -4568,7 +5187,7 @@ async function sendIntercomAsync(targetSession, opts) {
4568
5187
  return "debounced";
4569
5188
  }
4570
5189
  try {
4571
- 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 });
4572
5191
  const sessions = stdout.trim().split("\n").filter(Boolean);
4573
5192
  if (!sessions.includes(targetSession)) {
4574
5193
  logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
@@ -4582,7 +5201,7 @@ async function sendIntercomAsync(targetSession, opts) {
4582
5201
  recordResult("queued", "coordinator_no_inject");
4583
5202
  return "queued";
4584
5203
  }
4585
- const sessionState = getSessionState(targetSession);
5204
+ const sessionState = await getSessionStateAsync(targetSession);
4586
5205
  if (sessionState === "no_claude") {
4587
5206
  queueIntercom(targetSession, "claude not running in session", reason);
4588
5207
  const batched2 = recordDebounce(targetSession);
@@ -4598,14 +5217,14 @@ async function sendIntercomAsync(targetSession, opts) {
4598
5217
  return "queued";
4599
5218
  }
4600
5219
  try {
4601
- 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 });
4602
5221
  if (mode.stdout.trim() === "1") {
4603
5222
  logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
4604
- await execFileAsync("tmux", ["send-keys", "-t", targetSession, "q"], { timeout: 1e3 });
5223
+ await execFileAsync3("tmux", ["send-keys", "-t", targetSession, "q"], { timeout: 1e3 });
4605
5224
  }
4606
5225
  } catch {
4607
5226
  }
4608
- if (sessionHasPendingInput(targetSession)) {
5227
+ if (await sessionHasPendingInputAsync(targetSession)) {
4609
5228
  queueIntercom(targetSession, "non-empty input buffer at send time", reason);
4610
5229
  const batched2 = recordDebounce(targetSession);
4611
5230
  logIntercom(`QUEUED \u2192 ${targetSession} (non-empty input buffer)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
@@ -4673,8 +5292,21 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
4673
5292
  return false;
4674
5293
  }
4675
5294
  }
4676
- async function ensureWorktreeAsync(projectDir, employeeName, instance) {
4677
- 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 };
4678
5310
  }
4679
5311
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4680
5312
  employeeName = employeeName.toLowerCase();
@@ -4764,6 +5396,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4764
5396
  return { status: "failed", sessionName: failSession, error: compat.error };
4765
5397
  }
4766
5398
  }
5399
+ let acquiredHere = false;
4767
5400
  let effectiveInstance = opts?.instance;
4768
5401
  if (effectiveInstance === void 0 && opts?.autoInstance) {
4769
5402
  const free = findFreeInstance(
@@ -4778,6 +5411,7 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4778
5411
  error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
4779
5412
  };
4780
5413
  }
5414
+ acquiredHere = true;
4781
5415
  effectiveInstance = free === 0 ? void 0 : free;
4782
5416
  if (free !== null) {
4783
5417
  recordOrchestrationEventBestEffort({
@@ -4791,8 +5425,13 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4791
5425
  }
4792
5426
  }
4793
5427
  const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
5428
+ const releaseIfOwned = () => {
5429
+ if (acquiredHere) releaseSpawnLock(sessionName);
5430
+ };
4794
5431
  if (!opts?.autoInstance && !opts?.spawnLockHeld) {
4795
- if (!acquireSpawnLock(sessionName)) {
5432
+ if (acquireSpawnLock(sessionName)) {
5433
+ acquiredHere = true;
5434
+ } else {
4796
5435
  const reconcile = reconcileStaleSpawnLock(sessionName);
4797
5436
  if (reconcile.skip) {
4798
5437
  process.stderr.write(
@@ -4801,7 +5440,9 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4801
5440
  );
4802
5441
  return { status: "intercom_sent", sessionName };
4803
5442
  }
4804
- if (!acquireSpawnLock(sessionName)) {
5443
+ if (acquireSpawnLock(sessionName)) {
5444
+ acquiredHere = true;
5445
+ } else {
4805
5446
  process.stderr.write(
4806
5447
  `[ensureEmployee] spawn lock re-acquired by another process for ${sessionName} after stale-lock break \u2014 skipping duplicate spawn
4807
5448
  `
@@ -4811,8 +5452,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4811
5452
  }
4812
5453
  }
4813
5454
  if (isEmployeeAlive(sessionName)) {
4814
- if (!opts?.autoInstance) releaseSpawnLock(sessionName);
4815
- const result2 = sendIntercom(sessionName, { force: true, reason: "signal" });
5455
+ releaseIfOwned();
5456
+ const result2 = sendIntercom(sessionName, { force: true, reason: "signal", callerSessionScope: exeSession });
4816
5457
  if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
4817
5458
  return { status: "intercom_sent", sessionName };
4818
5459
  }
@@ -4854,10 +5495,36 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4854
5495
  process.stderr.write(`[tmux-routing] worktree isolation: ${employeeName} \u2192 ${wtPath}
4855
5496
  `);
4856
5497
  } else {
4857
- const msg = `Worktree isolation failed for code-role ${employeeName}; refusing to spawn in main checkout ${projectDir}`;
4858
- 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}
4859
5525
  `);
4860
- return { status: "failed", sessionName, error: msg };
5526
+ return { status: "failed", sessionName, error: gate.message };
5527
+ }
4861
5528
  }
4862
5529
  }
4863
5530
  } catch (e) {
@@ -4918,7 +5585,7 @@ async function ensureEmployeeAsync(employeeName, exeSession, projectDir, opts) {
4918
5585
  const emp = roster.find((e) => baseAgentName(e.name) === baseAgentName(employeeName));
4919
5586
  if (emp && !COORDINATOR_ROLES.has(emp.role)) {
4920
5587
  try {
4921
- const collisions = checkWorktreeCollisions(projectDir);
5588
+ const collisions = await checkWorktreeCollisionsAsync(projectDir);
4922
5589
  if (collisions.length > 0) {
4923
5590
  for (const c of collisions) {
4924
5591
  process.stderr.write(
@@ -4942,11 +5609,36 @@ async function ensureEmployeeAsync(employeeName, exeSession, projectDir, opts) {
4942
5609
  asyncOpts.cwd = wtPath;
4943
5610
  } else {
4944
5611
  const sessionName = employeeSessionName(employeeName, exeSession, asyncOpts.instance);
4945
- const msg = `Worktree isolation failed for code-role ${employeeName}; refusing to spawn in main checkout ${projectDir}`;
4946
- 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}
4947
5638
  `);
4948
- if (asyncOpts.spawnLockHeld) releaseSpawnLock(sessionName);
4949
- return { status: "failed", sessionName, error: msg };
5639
+ if (asyncOpts.spawnLockHeld) releaseSpawnLock(sessionName);
5640
+ return { status: "failed", sessionName, error: gate.message };
5641
+ }
4950
5642
  }
4951
5643
  }
4952
5644
  } catch (err) {
@@ -4966,11 +5658,21 @@ async function ensureEmployeeAsync(employeeName, exeSession, projectDir, opts) {
4966
5658
  );
4967
5659
  }
4968
5660
  const result = ensureEmployee(employeeName, exeSession, projectDir, asyncOpts);
4969
- if (asyncOpts.spawnLockHeld && result.status === "failed" && result.sessionName) {
5661
+ if (asyncOpts.spawnLockHeld && result.sessionName) {
4970
5662
  releaseSpawnLock(result.sessionName);
4971
5663
  }
4972
5664
  return result;
4973
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
+ }
4974
5676
  function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4975
5677
  const transport = getTransport();
4976
5678
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
@@ -5082,32 +5784,50 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5082
5784
  } catch {
5083
5785
  }
5084
5786
  const spawnCwd = opts?.cwd ?? projectDir;
5787
+ let disciplineRefusal = null;
5085
5788
  try {
5086
5789
  const inWorktree = spawnCwd.includes("/.worktrees/") || spawnCwd.includes("/.claude/worktrees/");
5087
- if (!inWorktree) {
5088
- const COORDINATOR_ROLES = /* @__PURE__ */ new Set(["COO", "CTO", "CMO"]);
5089
- const roster = loadEmployeesSync();
5090
- const emp = roster.find(
5091
- (e) => baseAgentName(e.name) === baseAgentName(employeeName)
5092
- );
5093
- const isCodeRole = emp != null && !COORDINATOR_ROLES.has(emp.role);
5094
- if (isCodeRole) {
5095
- const reason = opts?.cwd != null ? "preset_cwd" : "git_timeout";
5096
- process.stderr.write(
5097
- `[tmux-routing] WORKTREE BYPASSED: code-role ${employeeName} (role=${emp.role}) spawning in MAIN checkout ${spawnCwd} (reason=${reason}) \u2014 commits will land on main
5098
- `
5099
- );
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
+ `);
5100
5814
  recordOrchestrationEventBestEffort({
5101
- eventType: "worktree.bypassed",
5815
+ eventType: "worktree.refused",
5102
5816
  source: "tmux-routing.spawnEmployee",
5103
5817
  severity: "warn",
5104
5818
  agentId: employeeName,
5105
5819
  sessionScope: exeSession,
5106
5820
  instanceId: opts?.instance != null ? String(opts.instance) : null,
5107
5821
  tmuxSession: sessionName,
5108
- payload: { agent: employeeName, role: emp.role, cwd: spawnCwd, reason }
5822
+ payload: { agent: employeeName, role, cwd: spawnCwd, reason, rosterStatus: rosterResult.status }
5109
5823
  });
5110
- } 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);
5111
5831
  recordOrchestrationEventBestEffort({
5112
5832
  eventType: "worktree.bypassed",
5113
5833
  source: "tmux-routing.spawnEmployee",
@@ -5116,12 +5836,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5116
5836
  sessionScope: exeSession,
5117
5837
  instanceId: opts?.instance != null ? String(opts.instance) : null,
5118
5838
  tmuxSession: sessionName,
5119
- payload: { agent: employeeName, role: "unknown", cwd: spawnCwd, reason: "roster_miss" }
5839
+ payload: { agent: employeeName, role, cwd: spawnCwd, reason, optOut: viaOptOut, rosterStatus: rosterResult.status }
5120
5840
  });
5121
5841
  }
5122
5842
  }
5123
5843
  } catch {
5124
5844
  }
5845
+ if (disciplineRefusal) {
5846
+ releaseSpawnLock(sessionName);
5847
+ return { sessionName, error: disciplineRefusal };
5848
+ }
5125
5849
  const useExeAgent = !!(opts?.model && opts?.provider);
5126
5850
  const agentRtConfig = resolveSpawnRuntimeConfig(
5127
5851
  employeeName,
@@ -5493,13 +6217,6 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5493
6217
  }
5494
6218
 
5495
6219
  // src/lib/task-scope.ts
5496
- var VALID_SESSION_SCOPE_RE = /^[a-zA-Z0-9_]+$/;
5497
- function isValidSessionScope(scope) {
5498
- return typeof scope === "string" && scope.length > 0 && scope.length <= 64 && VALID_SESSION_SCOPE_RE.test(scope);
5499
- }
5500
- function sanitizeSessionScope(scope) {
5501
- return isValidSessionScope(scope) ? scope : null;
5502
- }
5503
6220
  function isAmbiguousMultiCoordinatorContext() {
5504
6221
  if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") return false;
5505
6222
  try {
@@ -5655,7 +6372,7 @@ function sessionScopeFilter(sessionScope, tableAlias, opts) {
5655
6372
  function strictSessionScopeFilter(sessionScope, tableAlias) {
5656
6373
  const autoDetect = sessionScope === void 0;
5657
6374
  const scope = autoDetect ? getCurrentSessionScope() : sessionScope;
5658
- if (!scope) {
6375
+ if (scope == null) {
5659
6376
  if (autoDetect && isAmbiguousMultiCoordinatorContext()) {
5660
6377
  return { sql: " AND 1=0", args: [] };
5661
6378
  }
@@ -5675,6 +6392,7 @@ export {
5675
6392
  claudeSupportsAgentFlag,
5676
6393
  PROVIDER_TABLE,
5677
6394
  DEFAULT_PROVIDER,
6395
+ VALID_SESSION_SCOPE_RE,
5678
6396
  isValidSessionScope,
5679
6397
  sanitizeSessionScope,
5680
6398
  isAmbiguousMultiCoordinatorContext,
@@ -5710,10 +6428,15 @@ export {
5710
6428
  checkLaneAffinity,
5711
6429
  resolveTask,
5712
6430
  createTaskCore,
6431
+ buildTaskRowsStatement,
5713
6432
  queryTaskRows,
5714
6433
  listTasks,
6434
+ __getLiveSessionsTmuxCalls,
6435
+ __resetLiveSessionsCache,
5715
6436
  isTmuxSessionAlive,
5716
6437
  TASK_ALREADY_CLAIMED_PREFIX,
6438
+ TASK_SIDECHAIN_WRITE_BLOCKED_PREFIX,
6439
+ instanceToken,
5717
6440
  VALID_TASK_STATUSES,
5718
6441
  checkStaleCompletion,
5719
6442
  updateTaskStatus,
@@ -5733,6 +6456,7 @@ export {
5733
6456
  STALE_REVIEW_AGE_MS,
5734
6457
  SIGNAL_FILE_TTL_MS,
5735
6458
  isStale,
6459
+ buildPendingReviewsCountStatement,
5736
6460
  countPendingReviews,
5737
6461
  countNewPendingReviewsSince,
5738
6462
  listPendingReviews,
@@ -5758,7 +6482,10 @@ export {
5758
6482
  findFreeInstance,
5759
6483
  verifyPaneAtCapacity,
5760
6484
  resolveSessionRuntime,
6485
+ classifyPaneSessionState,
5761
6486
  getSessionState,
6487
+ getSessionStateAsync,
6488
+ sessionHasPendingInputAsync,
5762
6489
  isSessionBusy,
5763
6490
  isExeSession,
5764
6491
  paneHasPendingInput,
@@ -5766,11 +6493,21 @@ export {
5766
6493
  sessionHasPendingInput,
5767
6494
  decideQueuedNudgeDrain,
5768
6495
  countScopedPendingReviews,
6496
+ countScopedOpenTasks,
6497
+ shouldDispositionWedged,
6498
+ buildScopedInProgressTaskQuery,
6499
+ findScopedInProgressTaskId,
6500
+ offlineIfEmpty,
6501
+ dispositionWedgedSessionTask,
6502
+ decideDrainSend,
5769
6503
  sendIntercom,
5770
6504
  sendIntercomAsync,
5771
6505
  notifyParentExe,
5772
6506
  notifyCoordinatorTaskCompletion,
6507
+ emitOptOutBypassWarning,
6508
+ worktreeIsolationGate,
5773
6509
  ensureEmployee,
5774
6510
  ensureEmployeeAsync,
6511
+ decideSpawnWorktreeDiscipline,
5775
6512
  spawnEmployee
5776
6513
  };