@askexenow/exe-os 0.9.322 → 0.9.324

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 (319) hide show
  1. package/deploy/compose/.env.customer.example +1 -1
  2. package/deploy/compose/.env.example +18 -1
  3. package/deploy/compose/PROTECTED_FILES.md +4 -1
  4. package/deploy/compose/add-routes.sh +22 -0
  5. package/deploy/compose/cloudflared/config.yml.example +6 -3
  6. package/deploy/compose/docker-compose.yml +36 -1
  7. package/deploy/compose/generate-env.ts +40 -2
  8. package/deploy/compose/sso-edge/default.conf.template +11 -0
  9. package/deploy/compose/sso-edge/sso-redirect.conf +90 -21
  10. package/deploy/stack-manifests/v0.9.json +110 -2
  11. package/dist/{active-agent-M5NJFEBM.js → active-agent-CVSB2EER.js} +3 -3
  12. package/dist/{active-agent-SHM4QSRJ.js → active-agent-IUVRZOSF.js} +3 -3
  13. package/dist/{agentic-ontology-HQKCU7LG.js → agentic-ontology-2QFPBIQP.js} +1 -1
  14. package/dist/backfill-gate-J7UXFNXU.js +19 -0
  15. package/dist/{backfill-metadata-CXWFCTBO.js → backfill-metadata-W3PRQPVV.js} +4 -4
  16. package/dist/{behaviors-J47D342W.js → behaviors-MQCADHAD.js} +5 -5
  17. package/dist/bin/agentic-ontology-backfill.js +5 -5
  18. package/dist/bin/agentic-reflection-backfill.js +6 -6
  19. package/dist/bin/agentic-semantic-label.js +5 -5
  20. package/dist/bin/backfill-conversations.js +11 -11
  21. package/dist/bin/backfill-responses.js +11 -11
  22. package/dist/bin/backfill-vectors.js +102 -22
  23. package/dist/bin/bulk-sync-postgres.js +14 -14
  24. package/dist/bin/cc-doctor.js +5 -5
  25. package/dist/bin/cleanup-stale-review-tasks.js +20 -13
  26. package/dist/bin/cli.js +28 -31
  27. package/dist/bin/customer-readiness.js +1 -0
  28. package/dist/bin/deferred-daemon-restart.js +80 -77
  29. package/dist/bin/exe-agent-config.js +3 -3
  30. package/dist/bin/exe-agent.js +5 -5
  31. package/dist/bin/exe-assign.js +10 -10
  32. package/dist/bin/exe-boot.js +32 -23
  33. package/dist/bin/exe-call.js +5 -5
  34. package/dist/bin/exe-cloud.js +120 -47
  35. package/dist/bin/exe-config-dump.js +3 -3
  36. package/dist/bin/exe-dispatch.js +48 -14
  37. package/dist/bin/exe-doctor.js +4 -4
  38. package/dist/bin/exe-export-behaviors.js +7 -7
  39. package/dist/bin/exe-forget.js +6 -6
  40. package/dist/bin/exe-gateway.js +8 -8
  41. package/dist/bin/exe-healthcheck.js +17 -8
  42. package/dist/bin/exe-heartbeat.js +20 -13
  43. package/dist/bin/exe-kill.js +23 -16
  44. package/dist/bin/exe-launch-agent.js +30 -27
  45. package/dist/bin/exe-new-employee.js +9 -9
  46. package/dist/bin/exe-pending-messages.js +21 -14
  47. package/dist/bin/exe-pending-notifications.js +20 -13
  48. package/dist/bin/exe-pending-reviews.js +20 -13
  49. package/dist/bin/exe-rename.js +5 -5
  50. package/dist/bin/exe-review.js +22 -15
  51. package/dist/bin/exe-search.js +8 -13
  52. package/dist/bin/exe-session-cleanup.js +39 -22
  53. package/dist/bin/exe-settings.js +15 -15
  54. package/dist/bin/exe-start-codex.js +51 -34
  55. package/dist/bin/exe-start-opencode.js +8 -8
  56. package/dist/bin/exe-status.js +21 -14
  57. package/dist/bin/exe-support.js +3 -3
  58. package/dist/bin/exe-team.js +3 -3
  59. package/dist/bin/exe-watchdog.js +278 -39
  60. package/dist/bin/git-sweep.js +21 -14
  61. package/dist/bin/graph-backfill.js +4 -4
  62. package/dist/bin/graph-export.js +5 -5
  63. package/dist/bin/import-history.js +7 -7
  64. package/dist/bin/install-launchd.js +2 -2
  65. package/dist/bin/install.js +121 -88
  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 +4 -4
  69. package/dist/bin/postgres-agentic-reflection-backfill.js +2 -2
  70. package/dist/bin/postgres-agentic-semantic-backfill.js +1 -1
  71. package/dist/bin/pre-publish.js +1 -1
  72. package/dist/bin/registry-proxy.js +1 -1
  73. package/dist/bin/scan-tasks.js +20 -13
  74. package/dist/bin/setup.js +1 -1
  75. package/dist/bin/shard-migrate.js +4 -4
  76. package/dist/bin/stack-update.js +4 -4
  77. package/dist/bin/vps-health-gate.js +1 -1
  78. package/dist/{capability-cards-H7H3MU74.js → capability-cards-STD3NMCR.js} +2 -2
  79. package/dist/{capacity-monitor-WAEHVBL7.js → capacity-monitor-6O3PWYNM.js} +21 -14
  80. package/dist/{catchup-brief-L42STIDR.js → catchup-brief-EDQ3VZCF.js} +23 -16
  81. package/dist/{chunk-HKSJSW2V.js → chunk-2E2NKFXK.js} +1 -1
  82. package/dist/{chunk-NYFPFOYM.js → chunk-2NY7LZT7.js} +1 -1
  83. package/dist/{chunk-DGFIXJ4S.js → chunk-2QD5M47V.js} +3 -3
  84. package/dist/{chunk-3M73L663.js → chunk-2YWUZPHE.js} +2 -2
  85. package/dist/{chunk-LB77CSLO.js → chunk-2ZW4AQ24.js} +3 -3
  86. package/dist/{chunk-E32KARET.js → chunk-376J36IW.js} +4 -4
  87. package/dist/{chunk-CWQAXO3O.js → chunk-5RACW3LX.js} +28 -3
  88. package/dist/{chunk-VSXGFHJY.js → chunk-5VT45EV6.js} +1 -1
  89. package/dist/{chunk-X62GD6QW.js → chunk-5YCOEPEX.js} +2 -2
  90. package/dist/{chunk-TKBWBHFV.js → chunk-6FWMLTF2.js} +1 -1
  91. package/dist/chunk-6IWLAQ7O.js +40 -0
  92. package/dist/{chunk-WFHYAKEU.js → chunk-6XAAJ5OV.js} +1 -1
  93. package/dist/{chunk-4Z47UMMM.js → chunk-7M6TXYRQ.js} +23 -23
  94. package/dist/{chunk-HWC5W34K.js → chunk-7PLMYXPG.js} +1 -1
  95. package/dist/{chunk-DDKDN4HV.js → chunk-A47MXY2I.js} +7 -0
  96. package/dist/{chunk-SQ5JWDJ2.js → chunk-AEFZEJKW.js} +1 -1
  97. package/dist/{chunk-657AQEPY.js → chunk-AVKZIG4A.js} +1 -1
  98. package/dist/{chunk-EPBB2OD3.js → chunk-BB5IKY7B.js} +2 -2
  99. package/dist/{chunk-3JW3ZXCP.js → chunk-BC7IIR3A.js} +65 -39
  100. package/dist/{chunk-WSYKLS2Y.js → chunk-BDJQWBWF.js} +3 -3
  101. package/dist/{chunk-VY7345RO.js → chunk-BMSAJ2IH.js} +1 -1
  102. package/dist/{chunk-OI63OVCR.js → chunk-C7B44DJI.js} +1 -1
  103. package/dist/{chunk-GA76RGSP.js → chunk-CS6MYGNB.js} +5 -5
  104. package/dist/{chunk-6N4LYEDI.js → chunk-CSPHV5DJ.js} +1 -1
  105. package/dist/{chunk-HGM6NZQH.js → chunk-DYUISOIT.js} +1 -1
  106. package/dist/{signal-paths-4GOIRLGH.js → chunk-EIKATSE4.js} +3 -4
  107. package/dist/{chunk-G5TP2D2U.js → chunk-EWDTTTZP.js} +3 -3
  108. package/dist/{chunk-TI6YX3L5.js → chunk-F2RK5YWB.js} +36 -27
  109. package/dist/{chunk-HV5CUDOE.js → chunk-F6TJIWN5.js} +1 -1
  110. package/dist/{chunk-WCCR5IFR.js → chunk-FPTBCPI3.js} +2 -2
  111. package/dist/{chunk-QVFJCFNM.js → chunk-FROUDYRT.js} +33 -2
  112. package/dist/{chunk-T6AUMO5X.js → chunk-GIFDGNIL.js} +1 -1
  113. package/dist/{chunk-OFR7KQ37.js → chunk-HAYDXMW6.js} +3 -3
  114. package/dist/{chunk-V2ESQU3S.js → chunk-HBKYKN3S.js} +3 -3
  115. package/dist/{chunk-LCZ55G5G.js → chunk-HZ5J2GFE.js} +136 -66
  116. package/dist/{chunk-EOPSIIEH.js → chunk-ID3V4BWS.js} +1 -1
  117. package/dist/{chunk-ABGDTMKC.js → chunk-JRD7ARQ6.js} +60 -2
  118. package/dist/{chunk-7FBPLVHM.js → chunk-KADUJ7PE.js} +3 -3
  119. package/dist/{chunk-KLG53B63.js → chunk-KJOQLNX2.js} +1 -1
  120. package/dist/{chunk-AZL3EUR3.js → chunk-KK67DSCO.js} +229 -29
  121. package/dist/{chunk-H2SD2EA7.js → chunk-KLDQFOFY.js} +23 -9
  122. package/dist/chunk-KPIN44GZ.js +67 -0
  123. package/dist/{chunk-PLBDXCYZ.js → chunk-KUJGADX6.js} +1491 -606
  124. package/dist/{chunk-GN3IWZDH.js → chunk-KWWHRJGG.js} +660 -539
  125. package/dist/{chunk-ERSFOJGC.js → chunk-LUOQCGBO.js} +20 -11
  126. package/dist/{chunk-CXXERHNC.js → chunk-LWAGAC3H.js} +1 -1
  127. package/dist/{chunk-7BZ6TOZN.js → chunk-MB2YT5VQ.js} +3 -3
  128. package/dist/{chunk-KXP3CUTL.js → chunk-MCEXBLLK.js} +3 -3
  129. package/dist/{chunk-A3KUGBDP.js → chunk-MIWPKWXQ.js} +1 -1
  130. package/dist/{chunk-7CUF27OT.js → chunk-MKIFYIXC.js} +1 -1
  131. package/dist/{chunk-ONKIWA3R.js → chunk-ML4S5SHZ.js} +3 -1
  132. package/dist/{chunk-BFHBHQFA.js → chunk-MWIX3YKA.js} +5 -5
  133. package/dist/{chunk-6UQRJCKK.js → chunk-NBDTQVWU.js} +2 -2
  134. package/dist/{chunk-KK4ZOYXJ.js → chunk-NGACCR2M.js} +80 -40
  135. package/dist/{chunk-CMDC5D7S.js → chunk-NLEW62VC.js} +6 -0
  136. package/dist/{chunk-QYLDCPNG.js → chunk-PGFYWVOB.js} +1 -1
  137. package/dist/{chunk-XEBN5CPD.js → chunk-PGUISLOT.js} +3 -3
  138. package/dist/{chunk-MYMA23KC.js → chunk-PJTEZLAM.js} +1 -1
  139. package/dist/{chunk-QNUHDEPI.js → chunk-POOMY3CS.js} +4 -4
  140. package/dist/{chunk-2GBGIXKH.js → chunk-PV7IG4V6.js} +1 -1
  141. package/dist/{chunk-SENJA7BZ.js → chunk-QDI77B3E.js} +54 -7
  142. package/dist/{chunk-XLNF3CIO.js → chunk-QGOMYIVT.js} +119 -24
  143. package/dist/{chunk-HIOO7DZ2.js → chunk-QQFZ32WS.js} +5 -5
  144. package/dist/{chunk-KL5WYWS3.js → chunk-SOXUF34Y.js} +2 -2
  145. package/dist/{chunk-6XM6PZXQ.js → chunk-SPO2RLOB.js} +1 -1
  146. package/dist/{chunk-ROMB2HH4.js → chunk-T3XHH5FE.js} +6 -6
  147. package/dist/{chunk-6SXJZHRJ.js → chunk-TA3A4UQL.js} +1 -1
  148. package/dist/{chunk-XP53WEXP.js → chunk-TGVKQ3QB.js} +3 -3
  149. package/dist/{chunk-XC7C3GQ2.js → chunk-TNS6U2K7.js} +30 -19
  150. package/dist/{chunk-JXEMAB4M.js → chunk-UAIMI7CK.js} +1 -1
  151. package/dist/{chunk-YIFZZYQD.js → chunk-UBYAKNRF.js} +2 -2
  152. package/dist/{chunk-6IBUQBMY.js → chunk-UXYQLOFW.js} +198 -39
  153. package/dist/{chunk-C4BIZXKB.js → chunk-UYOW7U24.js} +1 -1
  154. package/dist/{chunk-NFM6VHL7.js → chunk-V4VYIHSL.js} +1 -1
  155. package/dist/{chunk-QQOH3I4A.js → chunk-VDUHWQFA.js} +1 -1
  156. package/dist/{chunk-EVUXF2G4.js → chunk-W5GBB23I.js} +1 -1
  157. package/dist/{chunk-FANIBYUO.js → chunk-WF3J6GJZ.js} +2 -2
  158. package/dist/{chunk-NAHAOFTY.js → chunk-WGHTISNK.js} +3 -3
  159. package/dist/{chunk-F5FAD2YR.js → chunk-WGOAILWE.js} +2 -2
  160. package/dist/{chunk-LOE2N23P.js → chunk-WJICX53Z.js} +1 -1
  161. package/dist/{skill-refinement-YLIAY7V4.js → chunk-Y2KBHLEO.js} +3 -11
  162. package/dist/{chunk-PISGLNV7.js → chunk-Y34FDPOY.js} +5 -5
  163. package/dist/{chunk-ORC5AO7E.js → chunk-YBHOWK3L.js} +2 -2
  164. package/dist/{chunk-7SDS5VCL.js → chunk-ZDNJ3TSB.js} +7 -7
  165. package/dist/{chunk-CHSRSESS.js → chunk-ZRZ2Z6RZ.js} +1 -1
  166. package/dist/{cleanup-graph-garbage-MX5JDTJA.js → cleanup-graph-garbage-2YH52ADR.js} +4 -4
  167. package/dist/{co-activation-K44LGZ7A.js → co-activation-7F3J2GC7.js} +2 -2
  168. package/dist/{co-occurrence-N62HJTRP.js → co-occurrence-B36Q5GY3.js} +2 -2
  169. package/dist/{code-context-index-6YQ3DGCN.js → code-context-index-2PUBBQOE.js} +3 -3
  170. package/dist/{crdt-sync-BO76VVRX.js → crdt-sync-MXHPR6V6.js} +1 -1
  171. package/dist/{crm-bridge-BVTB6LZK.js → crm-bridge-CZLEW2LW.js} +1 -1
  172. package/dist/{crm-webhook-UZLTHXAS.js → crm-webhook-56MWGMRY.js} +2 -2
  173. package/dist/{cto-delegation-gate-NVMRONA2.js → cto-delegation-gate-26BD7NEE.js} +19 -12
  174. package/dist/{daemon-orchestration-JI3RD5EZ.js → daemon-orchestration-IBP6PWEG.js} +31 -16
  175. package/dist/{db-backup-ITRADB2J.js → db-backup-XLID57KW.js} +1 -1
  176. package/dist/{doc-graph-extractor-L7YESUSI.js → doc-graph-extractor-VBZ6XTTE.js} +2 -2
  177. package/dist/{documents-7BFXA6V3.js → documents-C5LYSJT3.js} +10 -10
  178. package/dist/{dreaming-NRTMPNPZ.js → dreaming-LMVDIC5C.js} +20 -13
  179. package/dist/{exe-drift-GLJIM7EI.js → exe-drift-T3K6WA47.js} +3 -3
  180. package/dist/{exe-export-THIAYHXK.js → exe-export-QCTASWXI.js} +7 -7
  181. package/dist/{exe-import-3Y4VOUYL.js → exe-import-WNP5A7VA.js} +7 -7
  182. package/dist/{exe-key-IPGY4RMU.js → exe-key-JM3JXE2E.js} +78 -75
  183. package/dist/{exe-snapshot-2ZEWCDYK.js → exe-snapshot-ODDJUEM5.js} +23 -16
  184. package/dist/{fast-db-init-TLYUWCDE.js → fast-db-init-O6A3TK3F.js} +1 -1
  185. package/dist/{founder-context-J362DNU4.js → founder-context-FYTF4IMD.js} +2 -2
  186. package/dist/gateway/index.js +9 -9
  187. package/dist/{git-staleness-BRDHTHNS.js → git-staleness-YA5J6VYJ.js} +2 -2
  188. package/dist/{global-procedures-FNUKY2LF.js → global-procedures-76NDWTYN.js} +4 -4
  189. package/dist/{graph-auto-extract-AJZUV3PO.js → graph-auto-extract-I7QL2TY5.js} +2 -2
  190. package/dist/hooks/bug-report-worker.js +22 -15
  191. package/dist/hooks/codex-stop-task-finalizer.js +22 -15
  192. package/dist/hooks/commit-complete.js +22 -15
  193. package/dist/hooks/error-recall.js +6 -6
  194. package/dist/hooks/exe-heartbeat-hook.js +3 -3
  195. package/dist/hooks/ingest-worker.js +6 -6
  196. package/dist/hooks/ingest.js +6 -6
  197. package/dist/hooks/instructions-loaded.js +4 -4
  198. package/dist/hooks/manifest.json +20 -20
  199. package/dist/hooks/notification.js +4 -4
  200. package/dist/hooks/post-compact.js +21 -14
  201. package/dist/hooks/post-tool-combined.js +8 -8
  202. package/dist/hooks/pre-compact.js +22 -21
  203. package/dist/hooks/pre-tool-use.js +29 -19
  204. package/dist/hooks/prompt-submit.js +89 -72
  205. package/dist/hooks/session-end.js +27 -26
  206. package/dist/hooks/session-start.js +13 -13
  207. package/dist/hooks/stop.js +28 -21
  208. package/dist/hooks/subagent-stop.js +24 -41
  209. package/dist/hooks/summary-worker.js +27 -25
  210. package/dist/index.js +30 -23
  211. package/dist/{installer-SC7CQBYY.js → installer-3EYHBKWB.js} +5 -5
  212. package/dist/{installer-UEF44TWQ.js → installer-3VZMDDMJ.js} +5 -5
  213. package/dist/{installer-2EKTDQZB.js → installer-GYABVCB7.js} +5 -5
  214. package/dist/intercom-queue-ZCNT4LYU.js +53 -0
  215. package/dist/lib/agent-config.js +7 -1
  216. package/dist/lib/cloud-sync.js +16 -14
  217. package/dist/lib/consolidation.js +5 -5
  218. package/dist/lib/database.js +10 -2
  219. package/dist/lib/db-daemon-client.js +3 -3
  220. package/dist/lib/db.js +10 -2
  221. package/dist/lib/device-registry.js +1 -1
  222. package/dist/lib/embed-worker.js +28 -0
  223. package/dist/lib/embedder.js +3 -3
  224. package/dist/lib/employee-templates.js +5 -5
  225. package/dist/lib/employees.js +2 -2
  226. package/dist/lib/exe-daemon-client.js +2 -2
  227. package/dist/lib/exe-daemon.js +393 -140
  228. package/dist/lib/hybrid-search.js +5 -5
  229. package/dist/lib/identity.js +2 -2
  230. package/dist/lib/license.js +2 -2
  231. package/dist/lib/messaging.js +20 -13
  232. package/dist/lib/registry-proxy.js +1 -1
  233. package/dist/lib/reminders.js +3 -3
  234. package/dist/lib/schedules.js +5 -5
  235. package/dist/lib/session-registry.js +6 -4
  236. package/dist/lib/skill-learning.js +6 -6
  237. package/dist/lib/store.js +4 -4
  238. package/dist/lib/task-router.js +3 -3
  239. package/dist/lib/tasks.js +21 -14
  240. package/dist/lib/tmux-routing.js +29 -12
  241. package/dist/lib/token-spend.js +3 -3
  242. package/dist/{license-gate-YUUSIPJ7.js → license-gate-73XGUIVP.js} +3 -3
  243. package/dist/{license-sync-LX73IEBH.js → license-sync-CG5YRUVH.js} +2 -2
  244. package/dist/mcp/register-tools.js +75 -72
  245. package/dist/mcp/server.js +96 -130
  246. package/dist/mcp/tools/complete-reminder.js +4 -4
  247. package/dist/mcp/tools/create-reminder.js +4 -4
  248. package/dist/mcp/tools/create-task.js +37 -18
  249. package/dist/mcp/tools/deactivate-behavior.js +7 -7
  250. package/dist/mcp/tools/list-reminders.js +4 -4
  251. package/dist/mcp/tools/list-tasks.js +23 -16
  252. package/dist/mcp/tools/send-message.js +22 -15
  253. package/dist/mcp/tools/update-task.js +22 -15
  254. package/dist/{mcp-http-config-MBG5AQNH.js → mcp-http-config-TT7PXZLX.js} +3 -3
  255. package/dist/{memory-cards-MGZ4P3AA.js → memory-cards-6NGUDUDN.js} +4 -4
  256. package/dist/{memory-graph-extractor-44P2ATID.js → memory-graph-extractor-FIMS3VQR.js} +3 -3
  257. package/dist/{memory-poisoning-defense-3CYFKNLA.js → memory-poisoning-defense-M3LOQ3XU.js} +2 -2
  258. package/dist/{memory-pressure-watchdog-6PFLEOUO.js → memory-pressure-watchdog-45GABDWH.js} +20 -13
  259. package/dist/{memory-queue-client-BZD6MXA5.js → memory-queue-client-BDYKFNB4.js} +3 -3
  260. package/dist/{memory-reflection-AYPJRPU7.js → memory-reflection-LANCFYK2.js} +7 -9
  261. package/dist/{notifications-BBQ23ORD.js → notifications-N5NKUYAU.js} +19 -12
  262. package/dist/{orchestration-events-O5P5NEKS.js → orchestration-events-KVNRNIPR.js} +3 -3
  263. package/dist/{orchestrator-SERM6FDY.js → orchestrator-VTF5BR26.js} +21 -14
  264. package/dist/{pipeline-router-T673WGL3.js → pipeline-router-KQAYWDPH.js} +3 -3
  265. package/dist/{plan-limits-QWBN2EKH.js → plan-limits-EUZI4X5W.js} +5 -5
  266. package/dist/{projection-worker-K6TILAZH.js → projection-worker-FG6G226D.js} +12 -2
  267. package/dist/{prospective-memory-XRNRMEDL.js → prospective-memory-FVDNUSIL.js} +2 -2
  268. package/dist/{reranker-3XC5UM4F.js → reranker-54Q3L32E.js} +1 -1
  269. package/dist/{retrieval-health-ULVVOIPL.js → retrieval-health-YJ7CXYVD.js} +1 -1
  270. package/dist/{review-polling-5YGDPJUX.js → review-polling-TKXYSXH6.js} +20 -13
  271. package/dist/runtime/index.js +23 -16
  272. package/dist/{session-events-D52HNUJK.js → session-events-WQHTP65R.js} +20 -13
  273. package/dist/{session-kill-telemetry-IBNCJSI7.js → session-kill-telemetry-2HSE7IGH.js} +20 -13
  274. package/dist/{session-scope-5FWZ4AYR.js → session-scope-RBKAC75W.js} +19 -12
  275. package/dist/{setup-wizard-KVTLSSRD.js → setup-wizard-AA6ZV57H.js} +1 -1
  276. package/dist/signal-paths-4G53QTII.js +11 -0
  277. package/dist/skill-refinement-QI35LMTZ.js +20 -0
  278. package/dist/{stack-update-OWWGTYRB.js → stack-update-BE6PX4AG.js} +9 -3
  279. package/dist/{steward-gate-3ZITRYVH.js → steward-gate-2XD5BXH6.js} +3 -3
  280. package/dist/{support-outbox-QPQQK7F7.js → support-outbox-MGUVVGHT.js} +96 -44
  281. package/dist/sweep-budget-WB2FKPYN.js +64 -0
  282. package/dist/{task-enforcement-L4YTDOOX.js → task-enforcement-DMOCBRLB.js} +191 -17
  283. package/dist/{task-scope-OIHLMQOQ.js → task-scope-ULBNFBFH.js} +25 -12
  284. package/dist/{tasks-crud-QUIIRWE3.js → tasks-crud-6SJOKES5.js} +25 -12
  285. package/dist/{tasks-notify-ANYLB5GU.js → tasks-notify-TESRABUM.js} +20 -13
  286. package/dist/{tasks-review-BWSCXFMB.js → tasks-review-INDAGLWJ.js} +27 -14
  287. package/dist/{telemetry-upload-XLDBCD7B.js → telemetry-upload-6XB3Q3IH.js} +7 -7
  288. package/dist/{token-budget-OQCLPV3A.js → token-budget-NOF27AZC.js} +3 -3
  289. package/dist/{tool-capability-index-3UWKZZED.js → tool-capability-index-EFBBPTBI.js} +1 -1
  290. package/dist/{tool-telemetry-AQTFWOWG.js → tool-telemetry-OEK6KQFF.js} +1 -1
  291. package/dist/tui/App.js +28 -21
  292. package/dist/{tui-data-3Y5L5LJB.js → tui-data-EG6LEZFF.js} +19 -12
  293. package/dist/{worker-gate-WJS2MFNA.js → worker-gate-YA35VMAN.js} +1 -1
  294. package/dist/{workflow-engine-EPPZ226V.js → workflow-engine-3AVKRE5I.js} +2 -2
  295. package/dist/{worktree-OYGDGJP7.js → worktree-72QYYL5L.js} +4 -4
  296. package/dist/{worktree-sweep-KV4H4766.js → worktree-sweep-GDISKGQ2.js} +5 -5
  297. package/package.json +1 -1
  298. package/release-notes.json +45 -45
  299. package/dist/chunk-4FVVJ7ME.js +0 -193
  300. package/dist/completion-nudge-dedup-DBZF24MM.js +0 -74
  301. package/dist/git-task-sweep-7GVUKDBW.js +0 -47
  302. package/dist/intercom-queue-6M5IRX3Y.js +0 -17
  303. /package/dist/{chunk-QTEHH4LG.js → chunk-24WPWCD6.js} +0 -0
  304. /package/dist/{chunk-IZY6V4FI.js → chunk-4O3N7I5Z.js} +0 -0
  305. /package/dist/{chunk-WXRVHXVP.js → chunk-7577GDDG.js} +0 -0
  306. /package/dist/{chunk-Q5YKWFVO.js → chunk-EWKZKO7F.js} +0 -0
  307. /package/dist/{chunk-DY3J6GQO.js → chunk-H43BPGIV.js} +0 -0
  308. /package/dist/{chunk-DQJ3WZRK.js → chunk-IHRXCNSC.js} +0 -0
  309. /package/dist/{chunk-RRQI5PSJ.js → chunk-NIB2N6L2.js} +0 -0
  310. /package/dist/{chunk-ZU6767WK.js → chunk-QMUSJESS.js} +0 -0
  311. /package/dist/{chunk-72LWCZTJ.js → chunk-RXS2LBKE.js} +0 -0
  312. /package/dist/{chunk-SJZT66LM.js → chunk-SNFYZFXY.js} +0 -0
  313. /package/dist/{chunk-SIJSFULW.js → chunk-TSYMWC53.js} +0 -0
  314. /package/dist/{core-memory-DARTILJG.js → core-memory-NKKP2XUB.js} +0 -0
  315. /package/dist/{entity-boost-VR5YOQCT.js → entity-boost-DRIUFTYB.js} +0 -0
  316. /package/dist/{message-queue-client-HVFR67QH.js → message-queue-client-4ERC7HLX.js} +0 -0
  317. /package/dist/{oauth-server-UQHCFVHT.js → oauth-server-PECJREY2.js} +0 -0
  318. /package/dist/{project-boot-22ZZL2GE.js → project-boot-SFKP2MFW.js} +0 -0
  319. /package/dist/{wiki-acl-EZMGL6KW.js → wiki-acl-QIBDNXSM.js} +0 -0
@@ -1,14 +1,18 @@
1
1
  import {
2
2
  ensureAgentSymlink
3
- } from "./chunk-HWC5W34K.js";
3
+ } from "./chunk-7PLMYXPG.js";
4
4
  import {
5
5
  ensureWorktree,
6
6
  getGitRoot,
7
7
  isWorktreeDirty
8
- } from "./chunk-KLG53B63.js";
8
+ } from "./chunk-KJOQLNX2.js";
9
9
  import {
10
- queueIntercom
11
- } from "./chunk-4FVVJ7ME.js";
10
+ incrementSkillSuccess
11
+ } from "./chunk-Y2KBHLEO.js";
12
+ import {
13
+ reviewSignalPath,
14
+ taskSignalDir
15
+ } from "./chunk-EIKATSE4.js";
12
16
  import {
13
17
  LEASE_MINUTES,
14
18
  leaseExpiryIso
@@ -18,8 +22,9 @@ import {
18
22
  serializeMessage
19
23
  } from "./chunk-4JERP7NT.js";
20
24
  import {
25
+ listSessions,
21
26
  registerSession
22
- } from "./chunk-ABGDTMKC.js";
27
+ } from "./chunk-JRD7ARQ6.js";
23
28
  import {
24
29
  listTmuxSessions
25
30
  } from "./chunk-SSTLTIF3.js";
@@ -28,23 +33,26 @@ import {
28
33
  } from "./chunk-RMOBQ6A5.js";
29
34
  import {
30
35
  recordOrchestrationEventBestEffort
31
- } from "./chunk-HV5CUDOE.js";
36
+ } from "./chunk-F6TJIWN5.js";
32
37
  import {
33
38
  assertRuntimeModelCompatible,
34
39
  getAgentRuntime,
35
40
  normalizeCcModelName,
36
41
  resolveSpawnRuntimeConfig
37
- } from "./chunk-QVFJCFNM.js";
42
+ } from "./chunk-FROUDYRT.js";
38
43
  import {
39
44
  RUNTIME_TABLE
40
45
  } from "./chunk-NGP6LSV2.js";
41
46
  import {
42
47
  PlanLimitError,
43
48
  assertEmployeeLimitSync
44
- } from "./chunk-EPBB2OD3.js";
49
+ } from "./chunk-BB5IKY7B.js";
45
50
  import {
46
51
  getSessionKey
47
52
  } from "./chunk-CVYC6DUW.js";
53
+ import {
54
+ writeMemoryViaDaemon
55
+ } from "./chunk-GIFDGNIL.js";
48
56
  import {
49
57
  getProjectName
50
58
  } from "./chunk-OPU3NYOO.js";
@@ -66,10 +74,10 @@ import {
66
74
  loadEmployees,
67
75
  loadEmployeesSync,
68
76
  offloadReadIfMain
69
- } from "./chunk-XLNF3CIO.js";
77
+ } from "./chunk-QGOMYIVT.js";
70
78
  import {
71
79
  loadDeviceId
72
- } from "./chunk-VY7345RO.js";
80
+ } from "./chunk-BMSAJ2IH.js";
73
81
  import {
74
82
  EXE_AI_DIR
75
83
  } from "./chunk-7Y5UDEVM.js";
@@ -81,9 +89,9 @@ import {
81
89
  // src/lib/tmux-routing.ts
82
90
  import { execFile, execFileSync as execFileSync2 } from "child_process";
83
91
  import { promisify } from "util";
84
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync7, appendFileSync as appendFileSync2, openSync, closeSync, writeSync, renameSync as renameSync2, constants, statSync as statSync2 } from "fs";
85
- import path7 from "path";
86
- import os5 from "os";
92
+ 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
+ import path8 from "path";
94
+ import os6 from "os";
87
95
  import { fileURLToPath } from "url";
88
96
 
89
97
  // src/lib/cc-help-cache.ts
@@ -142,56 +150,491 @@ function _resetCcHelpCache() {
142
150
  function _resetCcAgentSupportCache() {
143
151
  _resetCcHelpCache();
144
152
  }
145
- function claudeSupportsAgentFlag() {
146
- return /(^|\s)--agent(\b|=)/.test(getCcHelpOutputCached());
153
+ function claudeSupportsAgentFlag() {
154
+ return /(^|\s)--agent(\b|=)/.test(getCcHelpOutputCached());
155
+ }
156
+
157
+ // src/lib/provider-table.ts
158
+ var PROVIDER_TABLE = {
159
+ opencode: {
160
+ baseUrl: "https://opencode.ai/zen/go",
161
+ apiKeyEnv: "OPENCODE_API_KEY",
162
+ defaultModel: "kimi-k2.7-code"
163
+ }
164
+ };
165
+ var DEFAULT_PROVIDER = "default";
166
+ function detectActiveProvider(env = process.env) {
167
+ const baseUrl = env.ANTHROPIC_BASE_URL;
168
+ if (!baseUrl) return DEFAULT_PROVIDER;
169
+ for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
170
+ if (cfg.baseUrl === baseUrl) return name;
171
+ }
172
+ return DEFAULT_PROVIDER;
173
+ }
174
+
175
+ // src/lib/intercom-queue.ts
176
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync, existsSync as existsSync3, mkdirSync as mkdirSync2, appendFileSync, openSync, closeSync, writeSync, unlinkSync as unlinkSync2, constants, statSync } from "fs";
177
+ import path3 from "path";
178
+ import os3 from "os";
179
+
180
+ // src/lib/notifications.ts
181
+ import crypto from "crypto";
182
+ import path2 from "path";
183
+ import os2 from "os";
184
+ import {
185
+ readFileSync as readFileSync2,
186
+ readdirSync,
187
+ unlinkSync,
188
+ existsSync as existsSync2,
189
+ rmdirSync
190
+ } from "fs";
191
+ var CLEANUP_DAYS = 7;
192
+ async function writeNotification(notification) {
193
+ try {
194
+ const client = getClient();
195
+ const id = crypto.randomUUID();
196
+ const now = (/* @__PURE__ */ new Date()).toISOString();
197
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
198
+ await client.execute({
199
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
200
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
201
+ args: [
202
+ id,
203
+ notification.agentId,
204
+ notification.agentRole,
205
+ notification.event,
206
+ notification.project,
207
+ notification.summary,
208
+ notification.taskFile ?? null,
209
+ sessionScope,
210
+ now
211
+ ]
212
+ });
213
+ } catch (err) {
214
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
215
+ `);
216
+ }
217
+ }
218
+ async function readUnreadNotifications(agentFilter, sessionScope) {
219
+ try {
220
+ const client = getClient();
221
+ const conditions = ["read = 0"];
222
+ const args = [];
223
+ const scope = strictSessionScopeFilter(sessionScope ?? void 0);
224
+ if (agentFilter) {
225
+ conditions.push("agent_id = ?");
226
+ args.push(agentFilter);
227
+ }
228
+ const result = await client.execute({
229
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
230
+ FROM notifications
231
+ WHERE ${conditions.join(" AND ")}${scope.sql}
232
+ ORDER BY created_at ASC`,
233
+ args: [...args, ...scope.args]
234
+ });
235
+ return result.rows.map((r) => ({
236
+ id: String(r.id),
237
+ agentId: String(r.agent_id),
238
+ agentRole: String(r.agent_role),
239
+ event: String(r.event),
240
+ project: String(r.project),
241
+ summary: String(r.summary),
242
+ taskFile: r.task_file ? String(r.task_file) : void 0,
243
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
244
+ timestamp: String(r.created_at),
245
+ read: false
246
+ }));
247
+ } catch {
248
+ return [];
249
+ }
250
+ }
251
+ async function markAsRead(ids, sessionScope) {
252
+ if (ids.length === 0) return;
253
+ try {
254
+ const client = getClient();
255
+ const placeholders = ids.map(() => "?").join(", ");
256
+ const scope = strictSessionScopeFilter(sessionScope ?? void 0);
257
+ await client.execute({
258
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
259
+ args: [...ids, ...scope.args]
260
+ });
261
+ } catch {
262
+ }
263
+ }
264
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
265
+ try {
266
+ const client = getClient();
267
+ const scope = strictSessionScopeFilter(sessionScope);
268
+ await client.execute({
269
+ sql: `UPDATE notifications SET read = 1
270
+ WHERE task_file = ? AND read = 0${scope.sql}`,
271
+ args: [taskFile, ...scope.args]
272
+ });
273
+ } catch {
274
+ }
275
+ }
276
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
277
+ try {
278
+ const client = getClient();
279
+ const cutoff = new Date(
280
+ Date.now() - daysOld * 24 * 60 * 60 * 1e3
281
+ ).toISOString();
282
+ const scope = strictSessionScopeFilter(sessionScope ?? void 0);
283
+ const result = await client.execute({
284
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
285
+ args: [cutoff, ...scope.args]
286
+ });
287
+ return result.rowsAffected;
288
+ } catch {
289
+ return 0;
290
+ }
291
+ }
292
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
293
+ try {
294
+ const client = getClient();
295
+ const scope = strictSessionScopeFilter(sessionScope ?? void 0);
296
+ const result = await client.execute({
297
+ sql: `UPDATE notifications SET read = 1
298
+ WHERE read = 0
299
+ AND task_file IS NOT NULL
300
+ ${scope.sql}
301
+ AND task_file IN (
302
+ SELECT task_file FROM tasks WHERE status IN ('done', 'closed', 'cancelled')
303
+ )`,
304
+ args: [...scope.args]
305
+ });
306
+ return result.rowsAffected;
307
+ } catch {
308
+ return 0;
309
+ }
310
+ }
311
+ function formatNotifications(notifications) {
312
+ if (notifications.length === 0) return "";
313
+ const grouped = /* @__PURE__ */ new Map();
314
+ for (const n of notifications) {
315
+ const key = `${n.agentId}|${n.agentRole}`;
316
+ if (!grouped.has(key)) grouped.set(key, []);
317
+ grouped.get(key).push(n);
318
+ }
319
+ const lines = [];
320
+ lines.push(`## Notifications (${notifications.length} unread)
321
+ `);
322
+ for (const [key, items] of grouped) {
323
+ const [agentId, agentRole] = key.split("|");
324
+ lines.push(`**${agentId}** (${agentRole}):`);
325
+ for (const item of items) {
326
+ const ago = formatTimeAgo(item.timestamp);
327
+ const icon = eventIcon(item.event);
328
+ lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
329
+ }
330
+ lines.push("");
331
+ }
332
+ return lines.join("\n");
333
+ }
334
+ async function migrateJsonNotifications() {
335
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path2.join(os2.homedir(), ".exe-os");
336
+ const notifDir = path2.join(base, "notifications");
337
+ if (!existsSync2(notifDir)) return 0;
338
+ let migrated = 0;
339
+ try {
340
+ const files = readdirSync(notifDir).filter((f) => f.endsWith(".json"));
341
+ if (files.length === 0) return 0;
342
+ const client = getClient();
343
+ for (const file of files) {
344
+ try {
345
+ const filePath = path2.join(notifDir, file);
346
+ const data = JSON.parse(readFileSync2(filePath, "utf8"));
347
+ await client.execute({
348
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
349
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
350
+ args: [
351
+ crypto.randomUUID(),
352
+ data.agentId ?? "unknown",
353
+ data.agentRole ?? "unknown",
354
+ data.event ?? "session_summary",
355
+ data.project ?? "unknown",
356
+ data.summary ?? "",
357
+ data.taskFile ?? null,
358
+ null,
359
+ data.read ? 1 : 0,
360
+ data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
361
+ ]
362
+ });
363
+ unlinkSync(filePath);
364
+ migrated++;
365
+ } catch {
366
+ }
367
+ }
368
+ try {
369
+ const remaining = readdirSync(notifDir);
370
+ if (remaining.length === 0) {
371
+ rmdirSync(notifDir);
372
+ }
373
+ } catch {
374
+ }
375
+ } catch {
376
+ }
377
+ return migrated;
378
+ }
379
+ function eventIcon(event) {
380
+ switch (event) {
381
+ case "task_complete":
382
+ return "Completed:";
383
+ case "task_needs_fix":
384
+ return "Needs fix:";
385
+ case "session_summary":
386
+ return "Session:";
387
+ case "error_spike":
388
+ return "Errors:";
389
+ case "orphan_task":
390
+ return "Orphan:";
391
+ case "subtasks_complete":
392
+ return "Subtasks done:";
393
+ case "task_group_barrier":
394
+ return "Task group:";
395
+ case "capacity_relaunch":
396
+ return "Relaunched:";
397
+ }
398
+ }
399
+ function formatTimeAgo(timestamp) {
400
+ const diffMs = Date.now() - new Date(timestamp).getTime();
401
+ const mins = Math.floor(diffMs / 6e4);
402
+ if (mins < 1) return "just now";
403
+ if (mins < 60) return `${mins}m ago`;
404
+ const hours = Math.floor(mins / 60);
405
+ if (hours < 24) return `${hours}h ago`;
406
+ const days = Math.floor(hours / 24);
407
+ return `${days}d ago`;
408
+ }
409
+
410
+ // src/lib/intercom-queue.ts
411
+ var QUEUE_PATH = path3.join(os3.homedir(), ".exe-os", "intercom-queue.json");
412
+ var QUEUE_LOCK_PATH = `${QUEUE_PATH}.lock`;
413
+ var MAX_RETRIES = 10;
414
+ var TTL_MS = 60 * 60 * 1e3;
415
+ function ensureDir() {
416
+ const dir = path3.dirname(QUEUE_PATH);
417
+ if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
418
+ }
419
+ function acquireQueueLock() {
420
+ ensureDir();
421
+ const data = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
422
+ const deadline = Date.now() + 250;
423
+ while (Date.now() < deadline) {
424
+ try {
425
+ const fd = openSync(QUEUE_LOCK_PATH, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
426
+ writeSync(fd, data);
427
+ closeSync(fd);
428
+ return true;
429
+ } catch (err) {
430
+ if (err.code !== "EEXIST") return false;
431
+ try {
432
+ const lock = JSON.parse(readFileSync3(QUEUE_LOCK_PATH, "utf8"));
433
+ if (Date.now() - Number(lock.timestamp ?? 0) > 3e4) unlinkSync2(QUEUE_LOCK_PATH);
434
+ } catch {
435
+ try {
436
+ unlinkSync2(QUEUE_LOCK_PATH);
437
+ } catch {
438
+ }
439
+ }
440
+ }
441
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
442
+ }
443
+ return false;
444
+ }
445
+ function releaseQueueLock() {
446
+ try {
447
+ unlinkSync2(QUEUE_LOCK_PATH);
448
+ } catch {
449
+ }
450
+ }
451
+ function readQueue() {
452
+ try {
453
+ if (!existsSync3(QUEUE_PATH)) return [];
454
+ return JSON.parse(readFileSync3(QUEUE_PATH, "utf8"));
455
+ } catch {
456
+ return [];
457
+ }
458
+ }
459
+ function writeQueue(queue) {
460
+ ensureDir();
461
+ const tmp = `${QUEUE_PATH}.tmp`;
462
+ writeFileSync2(tmp, JSON.stringify(queue, null, 2), { mode: 384 });
463
+ renameSync(tmp, QUEUE_PATH);
464
+ }
465
+ function queueIntercom(targetSession, reason, intercomReason = "nudge") {
466
+ if (!acquireQueueLock()) return;
467
+ try {
468
+ const queue = readQueue();
469
+ const existing = queue.find((q) => q.targetSession === targetSession);
470
+ if (existing) {
471
+ existing.attempts++;
472
+ existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
473
+ existing.reason = reason;
474
+ existing.intercomReason = intercomReason;
475
+ } else {
476
+ queue.push({
477
+ targetSession,
478
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
479
+ attempts: 0,
480
+ reason,
481
+ intercomReason
482
+ });
483
+ }
484
+ writeQueue(queue);
485
+ } finally {
486
+ releaseQueueLock();
487
+ }
488
+ }
489
+ function notifyExpiredIntercom(item) {
490
+ const dashIdx = item.targetSession.indexOf("-");
491
+ const agentId = (dashIdx > 0 ? item.targetSession.slice(0, dashIdx) : item.targetSession).replace(/\d+$/, "") || item.targetSession;
492
+ const sessionScope = dashIdx > 0 ? item.targetSession.slice(dashIdx + 1) : item.targetSession;
493
+ void writeNotification({
494
+ agentId,
495
+ agentRole: "employee",
496
+ event: "error_spike",
497
+ project: "",
498
+ summary: `Intercom to ${item.targetSession} expired undelivered after 1h (${item.attempts} attempts, reason: ${item.reason}). Dropped from the retry queue \u2014 check the target session and re-send if still relevant.`,
499
+ sessionScope
500
+ });
501
+ }
502
+ var _draining = false;
503
+ function drainQueue(isSessionBusy2, sendKeys, notifyExpiry = notifyExpiredIntercom) {
504
+ if (_draining) {
505
+ logQueue("SKIP_DRAIN \u2014 previous drain still running (possible tmux hang)");
506
+ return { drained: 0, failed: 0 };
507
+ }
508
+ _draining = true;
509
+ const remaining = [];
510
+ let drained = 0;
511
+ let failed = 0;
512
+ let lockAcquired = false;
513
+ try {
514
+ lockAcquired = acquireQueueLock();
515
+ if (!lockAcquired) return { drained: 0, failed: 0 };
516
+ const queue = readQueue();
517
+ if (queue.length === 0) return { drained: 0, failed: 0 };
518
+ for (const item of queue) {
519
+ const age = Date.now() - new Date(item.queuedAt).getTime();
520
+ if (age > TTL_MS) {
521
+ logQueue(`EXPIRED \u2192 ${item.targetSession} (${Math.round(age / 6e4)}min old, reason: ${item.reason})`);
522
+ try {
523
+ notifyExpiry(item);
524
+ } catch (err) {
525
+ logQueue(`EXPIRY_NOTIFY_FAILED \u2192 ${item.targetSession}: ${err instanceof Error ? err.message : String(err)}`);
526
+ }
527
+ failed++;
528
+ continue;
529
+ }
530
+ try {
531
+ if (!isSessionBusy2(item.targetSession)) {
532
+ const success = sendKeys(item.targetSession, item);
533
+ if (success) {
534
+ logQueue(`DRAINED \u2192 ${item.targetSession} (after ${item.attempts} retries)`);
535
+ drained++;
536
+ continue;
537
+ }
538
+ }
539
+ } catch {
540
+ }
541
+ item.attempts++;
542
+ if (item.attempts >= MAX_RETRIES) {
543
+ logQueue(`PENDING \u2192 ${item.targetSession} (${MAX_RETRIES} retries exhausted, keeping queued, reason: ${item.reason})`);
544
+ item.attempts = MAX_RETRIES;
545
+ remaining.push(item);
546
+ failed++;
547
+ continue;
548
+ }
549
+ remaining.push(item);
550
+ }
551
+ writeQueue(remaining);
552
+ } finally {
553
+ if (lockAcquired) releaseQueueLock();
554
+ _draining = false;
555
+ }
556
+ return { drained, failed };
557
+ }
558
+ function drainForSession(targetSession, sendKeys) {
559
+ if (!acquireQueueLock()) return false;
560
+ try {
561
+ const queue = readQueue();
562
+ const match = queue.findIndex((q) => q.targetSession === targetSession);
563
+ if (match < 0) return false;
564
+ const item = queue[match];
565
+ const success = sendKeys(targetSession, item);
566
+ if (success) {
567
+ queue.splice(match, 1);
568
+ writeQueue(queue);
569
+ logQueue(`DRAINED \u2192 ${targetSession} (prompt-submit trigger)`);
570
+ return true;
571
+ }
572
+ return false;
573
+ } finally {
574
+ releaseQueueLock();
575
+ }
147
576
  }
148
-
149
- // src/lib/provider-table.ts
150
- var PROVIDER_TABLE = {
151
- opencode: {
152
- baseUrl: "https://opencode.ai/zen/go",
153
- apiKeyEnv: "OPENCODE_API_KEY",
154
- defaultModel: "kimi-k2.7-code"
577
+ function clearQueueForAgent(agentName) {
578
+ if (!acquireQueueLock()) return;
579
+ try {
580
+ const queue = readQueue();
581
+ const before = queue.length;
582
+ const filtered = queue.filter((q) => !q.targetSession.startsWith(`${agentName}-`));
583
+ if (filtered.length < before) {
584
+ writeQueue(filtered);
585
+ logQueue(`CLEARED ${before - filtered.length} stale item(s) for ${agentName}`);
586
+ }
587
+ } finally {
588
+ releaseQueueLock();
155
589
  }
156
- };
157
- var DEFAULT_PROVIDER = "default";
158
- function detectActiveProvider(env = process.env) {
159
- const baseUrl = env.ANTHROPIC_BASE_URL;
160
- if (!baseUrl) return DEFAULT_PROVIDER;
161
- for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
162
- if (cfg.baseUrl === baseUrl) return name;
590
+ }
591
+ function _resetDrainGuard() {
592
+ _draining = false;
593
+ }
594
+ var INTERCOM_LOG = path3.join(os3.homedir(), ".exe-os", "intercom.log");
595
+ var INTERCOM_LOG_MAX_BYTES = 1024 * 1024;
596
+ function logQueue(msg) {
597
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [queue] ${msg}
598
+ `;
599
+ process.stderr.write(`[intercom-queue] ${msg}
600
+ `);
601
+ try {
602
+ if (existsSync3(INTERCOM_LOG) && statSync(INTERCOM_LOG).size > INTERCOM_LOG_MAX_BYTES) {
603
+ renameSync(INTERCOM_LOG, `${INTERCOM_LOG}.1`);
604
+ }
605
+ appendFileSync(INTERCOM_LOG, line);
606
+ } catch {
163
607
  }
164
- return DEFAULT_PROVIDER;
165
608
  }
166
609
 
167
610
  // src/lib/tmux-routing.ts
168
- import { unlinkSync as unlinkSync3 } from "fs";
611
+ import { unlinkSync as unlinkSync4 } from "fs";
169
612
 
170
613
  // src/lib/routing-log.ts
171
- import { appendFileSync, mkdirSync as mkdirSync2, existsSync as existsSync2, statSync, renameSync } from "fs";
172
- import path2 from "path";
173
- import os2 from "os";
174
- var LOG_DIR = path2.join(os2.homedir(), ".exe-os", "logs");
175
- var LOG_FILE = path2.join(LOG_DIR, "routing.log");
614
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync4, statSync as statSync2, renameSync as renameSync2 } from "fs";
615
+ import path4 from "path";
616
+ import os4 from "os";
617
+ var LOG_DIR = path4.join(os4.homedir(), ".exe-os", "logs");
618
+ var LOG_FILE = path4.join(LOG_DIR, "routing.log");
176
619
  var dirChecked = false;
177
620
  var MAX_LOG_BYTES = 5 * 1024 * 1024;
178
- function ensureDir() {
621
+ function ensureDir2() {
179
622
  if (dirChecked) return;
180
- if (!existsSync2(LOG_DIR)) mkdirSync2(LOG_DIR, { recursive: true });
623
+ if (!existsSync4(LOG_DIR)) mkdirSync3(LOG_DIR, { recursive: true });
181
624
  dirChecked = true;
182
625
  }
183
626
  function rotateIfNeeded() {
184
627
  try {
185
- const stat = statSync(LOG_FILE);
628
+ const stat = statSync2(LOG_FILE);
186
629
  if (stat.size > MAX_LOG_BYTES) {
187
- renameSync(LOG_FILE, LOG_FILE + ".1");
630
+ renameSync2(LOG_FILE, LOG_FILE + ".1");
188
631
  }
189
632
  } catch {
190
633
  }
191
634
  }
192
635
  function logRouting(event) {
193
636
  try {
194
- ensureDir();
637
+ ensureDir2();
195
638
  const ts = (/* @__PURE__ */ new Date()).toISOString();
196
639
  const flag = event.mismatch ? " \u26A0\uFE0F MISMATCH" : "";
197
640
  const taskRef = event.taskId ? ` task=${event.taskId.slice(0, 8)}` : "";
@@ -206,7 +649,7 @@ function logRouting(event) {
206
649
  const line = `[${ts}] ${event.event} | ${event.caller}${taskRef}${assignee} | result=${event.result ?? "null"}${flag} | ${details}
207
650
  `;
208
651
  rotateIfNeeded();
209
- appendFileSync(LOG_FILE, line);
652
+ appendFileSync2(LOG_FILE, line);
210
653
  } catch {
211
654
  }
212
655
  }
@@ -275,16 +718,104 @@ function logTaskDispatch(opts) {
275
718
  }
276
719
 
277
720
  // src/lib/tasks-review.ts
278
- import path5 from "path";
279
- import { existsSync as existsSync5, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
721
+ import path6 from "path";
722
+ import { existsSync as existsSync7, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
723
+ import { homedir as homedir2 } from "os";
280
724
 
281
725
  // src/lib/tasks-crud.ts
282
- import crypto from "crypto";
283
- import path3 from "path";
284
- import os3 from "os";
726
+ import crypto2 from "crypto";
727
+ import path5 from "path";
728
+ import os5 from "os";
285
729
  import { execSync as execSync2 } from "child_process";
286
730
  import { mkdir, writeFile, appendFile } from "fs/promises";
287
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
731
+ import { existsSync as existsSync6, readFileSync as readFileSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
732
+
733
+ // src/lib/completion-nudge-dedup.ts
734
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
735
+ import { join, dirname } from "path";
736
+ import { homedir } from "os";
737
+ var COMPLETION_NUDGE_COOLDOWN_MS = Number(process.env.EXE_COMPLETION_NUDGE_COOLDOWN_MS) || 6 * 60 * 60 * 1e3;
738
+ var STALE_REVIEW_ESCALATION_COOLDOWN_MS = Number(process.env.EXE_STALE_REVIEW_ESCALATION_COOLDOWN_MS) || 7 * 24 * 60 * 60 * 1e3;
739
+ var completionLedger = {
740
+ path: join(homedir(), ".exe-os", "completion-nudge-state.json"),
741
+ cooldownMs: () => COMPLETION_NUDGE_COOLDOWN_MS,
742
+ state: null
743
+ };
744
+ var escalationLedger = {
745
+ path: join(homedir(), ".exe-os", "stale-review-escalation-state.json"),
746
+ cooldownMs: () => STALE_REVIEW_ESCALATION_COOLDOWN_MS,
747
+ state: null
748
+ };
749
+ function load(ledger) {
750
+ if (ledger.state) return ledger.state;
751
+ ledger.state = {};
752
+ try {
753
+ if (existsSync5(ledger.path)) {
754
+ const raw = JSON.parse(readFileSync4(ledger.path, "utf8"));
755
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
756
+ for (const [k, v] of Object.entries(raw)) {
757
+ if (typeof v === "number") ledger.state[k] = v;
758
+ }
759
+ }
760
+ }
761
+ } catch {
762
+ }
763
+ return ledger.state;
764
+ }
765
+ function persist(ledger, state) {
766
+ try {
767
+ mkdirSync4(dirname(ledger.path), { recursive: true });
768
+ writeFileSync3(ledger.path, JSON.stringify(state), "utf8");
769
+ } catch {
770
+ }
771
+ }
772
+ function prune(ledger, state, now) {
773
+ const cutoff = now - 2 * ledger.cooldownMs();
774
+ for (const [k, ts] of Object.entries(state)) {
775
+ if (ts < cutoff) delete state[k];
776
+ }
777
+ }
778
+ function claim(ledger, taskId, now) {
779
+ const state = load(ledger);
780
+ const last = state[taskId] ?? 0;
781
+ if (now - last < ledger.cooldownMs()) return false;
782
+ state[taskId] = now;
783
+ prune(ledger, state, now);
784
+ persist(ledger, state);
785
+ return true;
786
+ }
787
+ function clear(ledger, taskId) {
788
+ const state = load(ledger);
789
+ if (state[taskId] !== void 0) {
790
+ delete state[taskId];
791
+ persist(ledger, state);
792
+ }
793
+ }
794
+ function claimCompletionNudge(taskId, now = Date.now()) {
795
+ if (!taskId) return true;
796
+ return claim(completionLedger, taskId, now);
797
+ }
798
+ function recordCompletionNudge(taskId, now = Date.now()) {
799
+ if (!taskId) return;
800
+ const state = load(completionLedger);
801
+ state[taskId] = now;
802
+ prune(completionLedger, state, now);
803
+ persist(completionLedger, state);
804
+ }
805
+ function clearCompletionNudge(taskId) {
806
+ if (!taskId) return;
807
+ clear(completionLedger, taskId);
808
+ }
809
+ function claimStaleReviewEscalation(taskId, now = Date.now()) {
810
+ if (!taskId) return false;
811
+ return claim(escalationLedger, taskId, now);
812
+ }
813
+ function clearStaleReviewEscalation(taskId) {
814
+ if (!taskId) return;
815
+ clear(escalationLedger, taskId);
816
+ }
817
+
818
+ // src/lib/tasks-crud.ts
288
819
  var DEFAULT_TASK_PRIORITY = "p1";
289
820
  function normalizeTaskPriority(value) {
290
821
  const v = typeof value === "string" ? value.trim().toLowerCase() : "";
@@ -452,8 +983,18 @@ function buildKeywordIndex() {
452
983
  return idx;
453
984
  }
454
985
  var KEYWORD_INDEX = buildKeywordIndex();
455
- function checkLaneAffinity(title, context, assigneeName) {
456
- const employees = loadEmployeesSync();
986
+ var LANE_MANAGER_ROLES = {
987
+ COO: ["CTO", "CMO", "GM", "Principal Engineer", "Content Production Specialist", "Staff Code Reviewer", "AI Product Lead", "Specialist"],
988
+ CTO: ["Principal Engineer", "Staff Code Reviewer", "Specialist"],
989
+ CMO: ["Content Production Specialist", "Specialist"],
990
+ GM: ["Specialist"]
991
+ };
992
+ function roleCanManageLane(assigneeRole, matchedRoles) {
993
+ const managedRoles = LANE_MANAGER_ROLES[assigneeRole];
994
+ if (!managedRoles) return false;
995
+ return managedRoles.some((role) => matchedRoles.has(role));
996
+ }
997
+ function checkLaneAffinity(title, context, assigneeName, employees = loadEmployeesSync()) {
457
998
  const employee = employees.find((e) => e.name === assigneeName);
458
999
  if (!employee) return void 0;
459
1000
  const assigneeRole = employee.role;
@@ -466,7 +1007,7 @@ function checkLaneAffinity(title, context, assigneeName) {
466
1007
  }
467
1008
  if (matchedRoles.size === 0) return void 0;
468
1009
  if (matchedRoles.has(assigneeRole)) return void 0;
469
- if (assigneeRole === "COO") return void 0;
1010
+ if (roleCanManageLane(assigneeRole, matchedRoles)) return void 0;
470
1011
  const expectedRoles = Array.from(matchedRoles).join(" or ");
471
1012
  return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
472
1013
  }
@@ -479,7 +1020,9 @@ async function resolveTask(client, identifier, scopeSession) {
479
1020
  for (const row of rows) unique.set(String(row.id), row);
480
1021
  const crossSessionRows = [...unique.values()].filter((row) => {
481
1022
  const rowScope = row.session_scope === null || row.session_scope === void 0 ? null : String(row.session_scope);
482
- return rowScope !== null && rowScope !== currentScope;
1023
+ if (rowScope === null || rowScope === currentScope) return false;
1024
+ if (allowDeadScopeRescue(String(row.id), rowScope, currentScope)) return false;
1025
+ return true;
483
1026
  });
484
1027
  if (crossSessionRows.length === 0) return null;
485
1028
  const matches = crossSessionRows.slice(0, 5).map((row) => `${String(row.id).slice(0, 8)} "${String(row.title)}" [session:${String(row.session_scope)}]`).join(", ");
@@ -616,9 +1159,9 @@ ${f.context}
616
1159
  `;
617
1160
  }
618
1161
  async function writeTaskMarkdownAtomic(finalPath, content) {
619
- const dir = path3.dirname(finalPath);
620
- if (!existsSync3(dir)) await mkdir(dir, { recursive: true });
621
- const tmpPath = `${finalPath}.${crypto.randomBytes(6).toString("hex")}.tmp`;
1162
+ const dir = path5.dirname(finalPath);
1163
+ if (!existsSync6(dir)) await mkdir(dir, { recursive: true });
1164
+ const tmpPath = `${finalPath}.${crypto2.randomBytes(6).toString("hex")}.tmp`;
622
1165
  try {
623
1166
  await writeFile(tmpPath, content, "utf-8");
624
1167
  const { rename } = await import("fs/promises");
@@ -634,7 +1177,7 @@ async function writeTaskMarkdownAtomic(finalPath, content) {
634
1177
  }
635
1178
  async function createTaskCore(input) {
636
1179
  const client = getClient();
637
- const id = crypto.randomUUID();
1180
+ const id = crypto2.randomUUID();
638
1181
  const now = (/* @__PURE__ */ new Date()).toISOString();
639
1182
  const slug = slugify(input.title);
640
1183
  let earlySessionScope = null;
@@ -676,7 +1219,7 @@ async function createTaskCore(input) {
676
1219
  if (isCoordinatorSession) {
677
1220
  earlySessionScope = resolved;
678
1221
  } else {
679
- const { getSessionProject } = await import("./session-scope-5FWZ4AYR.js");
1222
+ const { getSessionProject } = await import("./session-scope-RBKAC75W.js");
680
1223
  const sessionProject = getSessionProject(resolved);
681
1224
  if (sessionProject && sessionProject !== input.projectName) {
682
1225
  scopeMismatchWarning = `session/project mismatch: session "${resolved}" owns "${sessionProject}" but task targets "${input.projectName}". Routed to default scope.`;
@@ -747,8 +1290,8 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
747
1290
  }
748
1291
  if (input.baseDir) {
749
1292
  try {
750
- await mkdir(path3.join(input.baseDir, "exe", "output"), { recursive: true });
751
- await mkdir(path3.join(input.baseDir, "exe", "research"), { recursive: true });
1293
+ await mkdir(path5.join(input.baseDir, "exe", "output"), { recursive: true });
1294
+ await mkdir(path5.join(input.baseDir, "exe", "research"), { recursive: true });
752
1295
  await ensureArchitectureDoc(input.baseDir, input.projectName);
753
1296
  await ensureGitignoreExe(input.baseDir);
754
1297
  } catch {
@@ -828,8 +1371,8 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
828
1371
  });
829
1372
  if (input.baseDir) {
830
1373
  try {
831
- const EXE_OS_DIR = path3.join(os3.homedir(), ".exe-os");
832
- const mdPath = path3.join(EXE_OS_DIR, taskFile);
1374
+ const EXE_OS_DIR = path5.join(os5.homedir(), ".exe-os");
1375
+ const mdPath = path5.join(EXE_OS_DIR, taskFile);
833
1376
  const reviewer = input.reviewer ?? input.assignedBy;
834
1377
  const mdContent = renderTaskMarkdown({
835
1378
  id,
@@ -925,7 +1468,7 @@ async function queryTaskRows(opts) {
925
1468
  }
926
1469
  if (opts.founderId !== null) {
927
1470
  try {
928
- const { founderScopeFilter } = await import("./founder-context-J362DNU4.js");
1471
+ const { founderScopeFilter } = await import("./founder-context-FYTF4IMD.js");
929
1472
  const founderFilter = founderScopeFilter(opts.founderId);
930
1473
  const cleaned = founderFilter.sql.replace(/^\s*AND\s+/, "");
931
1474
  conditions.push(cleaned);
@@ -1072,6 +1615,25 @@ function isTmuxSessionAlive(identifier) {
1072
1615
  }
1073
1616
  var DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
1074
1617
  var TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
1618
+ var VALID_TASK_STATUSES = [
1619
+ "open",
1620
+ "in_progress",
1621
+ "needs_review",
1622
+ "closed",
1623
+ "blocked",
1624
+ "cancelled"
1625
+ ];
1626
+ var VALID_TRANSITIONS = {
1627
+ open: ["in_progress", "needs_review", "cancelled", "blocked"],
1628
+ in_progress: ["in_progress", "open", "needs_review", "blocked", "cancelled"],
1629
+ blocked: ["open", "in_progress", "cancelled"],
1630
+ needs_review: ["closed", "in_progress", "open"],
1631
+ closed: [],
1632
+ // terminal — no transitions out
1633
+ cancelled: ["open"]
1634
+ // allow re-opening cancelled tasks
1635
+ };
1636
+ var STALE_FLIP_SUPPRESSION_MS = 48 * 60 * 60 * 1e3;
1075
1637
  function checkStaleCompletion(taskContext, taskCreatedAt) {
1076
1638
  if (!taskContext) return null;
1077
1639
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
@@ -1109,130 +1671,30 @@ async function updateTaskStatus(input) {
1109
1671
  if (input.status === "done") {
1110
1672
  input.status = "needs_review";
1111
1673
  }
1112
- if (previousStatus === "needs_review" && input.status !== "needs_review") {
1113
- try {
1114
- const { clearCompletionNudge } = await import("./completion-nudge-dedup-DBZF24MM.js");
1115
- clearCompletionNudge(String(row.id));
1116
- } catch {
1117
- }
1118
- }
1119
- const isReviewTransition = input.status === "needs_review" && previousStatus !== "needs_review" && (previousStatus === "open" || previousStatus === "in_progress");
1120
- const effectiveReviewer = row.reviewer ? String(row.reviewer) : row.assigned_by ? String(row.assigned_by) : "";
1121
- if (isReviewTransition && effectiveReviewer) {
1122
- const reviewer = effectiveReviewer;
1123
- try {
1124
- const { sendIntercom: sendIntercom2, employeeSessionName: employeeSessionName2, resolveExeSession: resolveExeSession2, isExeSession: isExeSession2 } = await import("./lib/tmux-routing.js");
1125
- const exeSession = row.session_scope ? String(row.session_scope) : resolveExeSession2();
1126
- if (exeSession) {
1127
- if (isCoordinatorName(reviewer)) {
1128
- if (isExeSession2(exeSession)) {
1129
- sendIntercom2(exeSession, { force: true, reason: "completion" });
1130
- process.stderr.write(
1131
- `[tasks-crud] Review intercom sent to ${exeSession}: "${String(row.title)}" by ${String(row.assigned_to)}
1132
- `
1133
- );
1134
- }
1135
- } else {
1136
- const reviewerSession = employeeSessionName2(reviewer, exeSession);
1137
- sendIntercom2(reviewerSession, { force: true, reason: "completion" });
1138
- process.stderr.write(
1139
- `[tasks-crud] Review intercom sent to ${reviewerSession}: "${String(row.title)}" by ${String(row.assigned_to)}
1140
- `
1141
- );
1142
- }
1143
- }
1144
- } catch (err) {
1145
- process.stderr.write(
1146
- `[tasks-crud] Review intercom failed for ${reviewer}: ${err instanceof Error ? err.message : String(err)}
1674
+ if (!VALID_TASK_STATUSES.includes(input.status)) {
1675
+ process.stderr.write(
1676
+ `[tasks-crud] REJECTED invalid status literal "${input.status}" for task ${taskId} (valid: ${VALID_TASK_STATUSES.join(", ")})
1147
1677
  `
1148
- );
1149
- }
1150
- try {
1151
- const { recordCompletionNudge } = await import("./completion-nudge-dedup-DBZF24MM.js");
1152
- recordCompletionNudge(String(row.id));
1153
- } catch {
1154
- }
1155
- try {
1156
- const { reviewSignalPath, taskSignalDir } = await import("./signal-paths-4GOIRLGH.js");
1157
- const scope = row.session_scope ? String(row.session_scope) : "default";
1158
- const dir = taskSignalDir(scope);
1159
- const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync3 } = await import("fs");
1160
- mkdirSync4(dir, { recursive: true });
1161
- writeFileSync3(
1162
- reviewSignalPath(scope, reviewer, String(row.id)),
1163
- JSON.stringify({
1164
- task_id: String(row.id),
1165
- title: String(row.title),
1166
- assigned_to: String(row.assigned_to),
1167
- reviewer,
1168
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1169
- }),
1170
- "utf-8"
1171
- );
1172
- } catch {
1173
- }
1174
- try {
1175
- const { writeNotification: writeNotification2 } = await import("./notifications-BBQ23ORD.js");
1176
- await writeNotification2({
1177
- agentId: reviewer,
1178
- agentRole: isCoordinatorName(reviewer) ? "COO" : "manager",
1179
- event: "task_complete",
1180
- project: String(row.project_name ?? ""),
1181
- summary: `${String(row.assigned_to)} completed "${String(row.title)}" \u2014 needs your review.`,
1182
- taskFile,
1183
- sessionScope: row.session_scope ? String(row.session_scope) : void 0
1184
- });
1185
- } catch {
1186
- }
1678
+ );
1679
+ throw new Error(
1680
+ `Invalid task status "${input.status}". Valid statuses: ${VALID_TASK_STATUSES.join(", ")} ("done" is accepted as an alias for "needs_review").`
1681
+ );
1187
1682
  }
1188
- const isBlockedTransition = input.status === "blocked" && previousStatus !== "blocked";
1189
- if (isBlockedTransition && row.assigned_by) {
1190
- const dispatcher = String(row.assigned_by);
1191
- try {
1192
- const { sendIntercom: sendIntercom2, employeeSessionName: employeeSessionName2, resolveExeSession: resolveExeSession2, isExeSession: isExeSession2 } = await import("./lib/tmux-routing.js");
1193
- const exeSession = row.session_scope ? String(row.session_scope) : resolveExeSession2();
1194
- if (exeSession) {
1195
- if (isCoordinatorName(dispatcher)) {
1196
- if (isExeSession2(exeSession)) {
1197
- sendIntercom2(exeSession, { force: true, reason: "completion" });
1198
- process.stderr.write(
1199
- `[tasks-crud] Blocked intercom sent to ${exeSession}: "${String(row.title)}" blocked by ${String(row.assigned_to)}
1200
- `
1201
- );
1202
- }
1203
- } else {
1204
- const dispatcherSession = employeeSessionName2(dispatcher, exeSession);
1205
- sendIntercom2(dispatcherSession, { force: true, reason: "completion" });
1206
- process.stderr.write(
1207
- `[tasks-crud] Blocked intercom sent to ${dispatcherSession}: "${String(row.title)}" blocked by ${String(row.assigned_to)}
1208
- `
1209
- );
1210
- }
1211
- }
1212
- } catch (err) {
1213
- process.stderr.write(
1214
- `[tasks-crud] Blocked intercom failed for ${dispatcher}: ${err instanceof Error ? err.message : String(err)}
1683
+ if (!VALID_TASK_STATUSES.includes(previousStatus)) {
1684
+ process.stderr.write(
1685
+ `[tasks-crud] WARNING: task ${taskId} has unknown stored status "${previousStatus}" (not in the 6-status model) \u2014 allowing transition to "${input.status}" to heal the row
1215
1686
  `
1216
- );
1217
- }
1687
+ );
1218
1688
  }
1219
- const VALID_TRANSITIONS = {
1220
- open: ["in_progress", "needs_review", "cancelled", "blocked"],
1221
- in_progress: ["in_progress", "open", "needs_review", "blocked", "cancelled"],
1222
- blocked: ["open", "in_progress", "cancelled"],
1223
- needs_review: ["closed", "in_progress", "open"],
1224
- closed: [],
1225
- // terminal — no transitions out
1226
- cancelled: ["open"]
1227
- // allow re-opening cancelled tasks
1228
- };
1229
- const currentStatus = String(row.status);
1230
- const validNext = VALID_TRANSITIONS[currentStatus];
1689
+ const validNext = VALID_TRANSITIONS[previousStatus];
1231
1690
  if (validNext && !validNext.includes(input.status)) {
1232
1691
  throw new Error(
1233
- `Invalid task transition: ${currentStatus} \u2192 ${input.status}. Valid transitions from "${currentStatus}": [${validNext.join(", ")}]`
1692
+ `Invalid task transition: ${previousStatus} \u2192 ${input.status}. Valid transitions from "${previousStatus}": [${validNext.join(", ")}]`
1234
1693
  );
1235
1694
  }
1695
+ const isReviewTransition = input.status === "needs_review" && previousStatus !== "needs_review" && (previousStatus === "open" || previousStatus === "in_progress");
1696
+ const effectiveReviewer = row.reviewer ? String(row.reviewer) : row.assigned_by ? String(row.assigned_by) : "";
1697
+ const isBlockedTransition = input.status === "blocked" && previousStatus !== "blocked";
1236
1698
  if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
1237
1699
  process.stderr.write(
1238
1700
  `[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
@@ -1257,6 +1719,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1257
1719
  }
1258
1720
  }
1259
1721
  const hasStructuredFields = input.filesChanged || input.decisions || input.commits || input.testsStatus || input.buildStatus || input.prNumber || input.openQuestions;
1722
+ let structuredSerialized = null;
1260
1723
  if (hasStructuredFields && (requestedStatus === "done" || input.status === "needs_review")) {
1261
1724
  const agentId = String(row.assigned_to ?? "unknown");
1262
1725
  const typedResult = buildTaskResult({
@@ -1271,20 +1734,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1271
1734
  pr_number: input.prNumber,
1272
1735
  open_questions: input.openQuestions
1273
1736
  });
1274
- const serialized = serializeMessage(typedResult);
1275
- try {
1276
- await client.execute({
1277
- sql: "UPDATE tasks SET structured_result = ?, updated_at = ? WHERE id = ?",
1278
- args: [serialized, now, taskId]
1279
- });
1280
- } catch {
1281
- process.stderr.write(
1282
- `[tasks-crud] structured_result column unavailable \u2014 embedding in result text
1283
- `
1284
- );
1285
- input.result = `<!-- STRUCTURED_RESULT:${serialized} -->
1286
- ${input.result ?? ""}`;
1287
- }
1737
+ structuredSerialized = serializeMessage(typedResult);
1288
1738
  }
1289
1739
  const shouldWriteCompletionMemory = shouldNormalizeTaskResult(requestedStatus, input.status) && previousStatus !== input.status && (requestedStatus === "done" || input.status === "needs_review");
1290
1740
  if (shouldNormalizeTaskResult(requestedStatus, input.status)) {
@@ -1312,7 +1762,7 @@ ${input.result ?? ""}`;
1312
1762
  const leaseExpiry = leaseExpiryIso(now, leaseMinutes);
1313
1763
  const estMin = input.estimatedMinutes != null ? Math.max(1, Math.round(input.estimatedMinutes)) : null;
1314
1764
  const estConf = input.estimateConfidence != null ? Math.max(0, Math.min(1, input.estimateConfidence)) : null;
1315
- const claim = await client.execute({
1765
+ const claim2 = await client.execute({
1316
1766
  sql: `UPDATE tasks
1317
1767
  SET status = 'in_progress', assigned_tmux = ?, updated_at = ?,
1318
1768
  claimed_by = ?, claimed_at = ?, last_heartbeat_at = ?,
@@ -1324,7 +1774,7 @@ ${input.result ?? ""}`;
1324
1774
  WHERE id = ? AND status = 'open'${claimGuardSql}`,
1325
1775
  args: [tmuxSession, now, claimAgent, now, now, leaseExpiry, now, estMin, estConf, taskId, ...claimGuardArgs]
1326
1776
  });
1327
- if (claim.rowsAffected === 0) {
1777
+ if (claim2.rowsAffected === 0) {
1328
1778
  const current = await client.execute({
1329
1779
  sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
1330
1780
  args: [taskId]
@@ -1338,10 +1788,7 @@ ${input.result ?? ""}`;
1338
1788
  `[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
1339
1789
  `
1340
1790
  );
1341
- await client.execute({
1342
- sql: "UPDATE tasks SET status = 'open', assigned_tmux = NULL, claimed_by = NULL, claimed_at = NULL, last_heartbeat_at = NULL, lease_expires_at = NULL, updated_at = ? WHERE id = ?",
1343
- args: [now, taskId]
1344
- });
1791
+ await reopenTask(taskId, `dead claim auto-release (was ${claimedBySession})`);
1345
1792
  const retried = await client.execute({
1346
1793
  sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ?,
1347
1794
  claimed_by = ?, claimed_at = ?, last_heartbeat_at = ?,
@@ -1558,17 +2005,165 @@ ${input.result ?? ""}`;
1558
2005
  }
1559
2006
  }
1560
2007
  }
1561
- if (input.result) {
1562
- await client.execute({
1563
- sql: `UPDATE tasks SET status = ?, result = ?, updated_at = ?${actualMinutesClause} WHERE id = ?`,
1564
- args: [input.status, input.result, now, ...actualMinutesArgs, taskId]
1565
- });
1566
- } else {
1567
- await client.execute({
1568
- sql: `UPDATE tasks SET status = ?, updated_at = ?${actualMinutesClause} WHERE id = ?`,
1569
- args: [input.status, now, ...actualMinutesArgs, taskId]
1570
- });
1571
- }
2008
+ const buildStatusStatement = () => input.result ? {
2009
+ sql: `UPDATE tasks SET status = ?, result = ?, updated_at = ?${actualMinutesClause} WHERE id = ?`,
2010
+ args: [input.status, input.result, now, ...actualMinutesArgs, taskId]
2011
+ } : {
2012
+ sql: `UPDATE tasks SET status = ?, updated_at = ?${actualMinutesClause} WHERE id = ?`,
2013
+ args: [input.status, now, ...actualMinutesArgs, taskId]
2014
+ };
2015
+ const writeStatements = [];
2016
+ if (structuredSerialized) {
2017
+ writeStatements.push({
2018
+ sql: "UPDATE tasks SET structured_result = ?, updated_at = ? WHERE id = ?",
2019
+ args: [structuredSerialized, now, taskId]
2020
+ });
2021
+ }
2022
+ writeStatements.push(buildStatusStatement());
2023
+ try {
2024
+ await client.batch(writeStatements, "write");
2025
+ } catch (err) {
2026
+ const errMsg2 = err instanceof Error ? err.message : String(err);
2027
+ if (structuredSerialized && /structured_result/i.test(errMsg2)) {
2028
+ process.stderr.write(
2029
+ `[tasks-crud] structured_result column unavailable \u2014 embedding in result text
2030
+ `
2031
+ );
2032
+ input.result = `<!-- STRUCTURED_RESULT:${structuredSerialized} -->
2033
+ ${input.result ?? ""}`;
2034
+ await client.batch([buildStatusStatement()], "write");
2035
+ } else {
2036
+ throw err;
2037
+ }
2038
+ }
2039
+ if (previousStatus === "needs_review" && input.status !== "needs_review") {
2040
+ try {
2041
+ clearCompletionNudge(taskId);
2042
+ } catch (err) {
2043
+ process.stderr.write(
2044
+ `[tasks-crud] clearCompletionNudge failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}
2045
+ `
2046
+ );
2047
+ }
2048
+ }
2049
+ if (isReviewTransition && effectiveReviewer) {
2050
+ const reviewer = effectiveReviewer;
2051
+ const lastTouchedRaw = row.updated_at ?? row.created_at;
2052
+ const lastTouchedMs = lastTouchedRaw ? Date.parse(String(lastTouchedRaw)) : NaN;
2053
+ const isStaleFlip = Number.isFinite(lastTouchedMs) && Date.parse(now) - lastTouchedMs > STALE_FLIP_SUPPRESSION_MS;
2054
+ const isSelfReview = reviewer === String(row.assigned_to);
2055
+ const suppressNudgeStorm = isStaleFlip || isSelfReview;
2056
+ if (suppressNudgeStorm) {
2057
+ process.stderr.write(
2058
+ `[tasks-crud] Nudge storm suppressed for ${taskId} (${isSelfReview ? "self-review: assigned_to === reviewer" : "stale flip: last touched >48h ago"}) \u2014 notification row only
2059
+ `
2060
+ );
2061
+ }
2062
+ if (!suppressNudgeStorm) {
2063
+ try {
2064
+ const exeSession = row.session_scope ? String(row.session_scope) : resolveExeSession();
2065
+ if (exeSession) {
2066
+ if (isCoordinatorName(reviewer)) {
2067
+ if (isExeSession(exeSession)) {
2068
+ sendIntercom(exeSession, { force: true, reason: "completion" });
2069
+ process.stderr.write(
2070
+ `[tasks-crud] Review intercom sent to ${exeSession}: "${String(row.title)}" by ${String(row.assigned_to)}
2071
+ `
2072
+ );
2073
+ }
2074
+ } else {
2075
+ const reviewerSession = employeeSessionName(reviewer, exeSession);
2076
+ sendIntercom(reviewerSession, { force: true, reason: "completion" });
2077
+ process.stderr.write(
2078
+ `[tasks-crud] Review intercom sent to ${reviewerSession}: "${String(row.title)}" by ${String(row.assigned_to)}
2079
+ `
2080
+ );
2081
+ }
2082
+ }
2083
+ } catch (err) {
2084
+ process.stderr.write(
2085
+ `[tasks-crud] Review intercom failed for ${reviewer}: ${err instanceof Error ? err.message : String(err)}
2086
+ `
2087
+ );
2088
+ }
2089
+ }
2090
+ try {
2091
+ recordCompletionNudge(taskId);
2092
+ } catch (err) {
2093
+ process.stderr.write(
2094
+ `[tasks-crud] recordCompletionNudge failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}
2095
+ `
2096
+ );
2097
+ }
2098
+ if (!suppressNudgeStorm) {
2099
+ try {
2100
+ const scope = row.session_scope ? String(row.session_scope) : "default";
2101
+ const dir = taskSignalDir(scope);
2102
+ mkdirSync5(dir, { recursive: true });
2103
+ writeFileSync4(
2104
+ reviewSignalPath(scope, reviewer, taskId),
2105
+ JSON.stringify({
2106
+ task_id: taskId,
2107
+ title: String(row.title),
2108
+ assigned_to: String(row.assigned_to),
2109
+ reviewer,
2110
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2111
+ }),
2112
+ "utf-8"
2113
+ );
2114
+ } catch (err) {
2115
+ process.stderr.write(
2116
+ `[tasks-crud] Review signal file write failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}
2117
+ `
2118
+ );
2119
+ }
2120
+ }
2121
+ try {
2122
+ await writeNotification({
2123
+ agentId: reviewer,
2124
+ agentRole: isCoordinatorName(reviewer) ? "COO" : "manager",
2125
+ event: "task_complete",
2126
+ project: String(row.project_name ?? ""),
2127
+ summary: `${String(row.assigned_to)} completed "${String(row.title)}" \u2014 needs your review.`,
2128
+ taskFile,
2129
+ sessionScope: row.session_scope ? String(row.session_scope) : void 0
2130
+ });
2131
+ } catch (err) {
2132
+ process.stderr.write(
2133
+ `[tasks-crud] Completion notification write failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}
2134
+ `
2135
+ );
2136
+ }
2137
+ }
2138
+ if (isBlockedTransition && row.assigned_by) {
2139
+ const dispatcher = String(row.assigned_by);
2140
+ try {
2141
+ const exeSession = row.session_scope ? String(row.session_scope) : resolveExeSession();
2142
+ if (exeSession) {
2143
+ if (isCoordinatorName(dispatcher)) {
2144
+ if (isExeSession(exeSession)) {
2145
+ sendIntercom(exeSession, { force: true, reason: "completion" });
2146
+ process.stderr.write(
2147
+ `[tasks-crud] Blocked intercom sent to ${exeSession}: "${String(row.title)}" blocked by ${String(row.assigned_to)}
2148
+ `
2149
+ );
2150
+ }
2151
+ } else {
2152
+ const dispatcherSession = employeeSessionName(dispatcher, exeSession);
2153
+ sendIntercom(dispatcherSession, { force: true, reason: "completion" });
2154
+ process.stderr.write(
2155
+ `[tasks-crud] Blocked intercom sent to ${dispatcherSession}: "${String(row.title)}" blocked by ${String(row.assigned_to)}
2156
+ `
2157
+ );
2158
+ }
2159
+ }
2160
+ } catch (err) {
2161
+ process.stderr.write(
2162
+ `[tasks-crud] Blocked intercom failed for ${dispatcher}: ${err instanceof Error ? err.message : String(err)}
2163
+ `
2164
+ );
2165
+ }
2166
+ }
1572
2167
  if (input.status === "cancelled" || input.status === "closed") {
1573
2168
  try {
1574
2169
  await client.execute({
@@ -1592,7 +2187,6 @@ ${input.result ?? ""}`;
1592
2187
  }
1593
2188
  if (shouldWriteCompletionMemory && input.result) {
1594
2189
  try {
1595
- const { writeMemoryViaDaemon } = await import("./memory-queue-client-BZD6MXA5.js");
1596
2190
  await writeMemoryViaDaemon({
1597
2191
  raw_text: input.result,
1598
2192
  agent_id: String(row.assigned_to),
@@ -1695,19 +2289,25 @@ ${input.result ?? ""}`;
1695
2289
  }
1696
2290
  if (input.status === "done" || input.status === "needs_review") {
1697
2291
  try {
1698
- const { incrementSkillSuccess } = await import("./skill-refinement-YLIAY7V4.js");
1699
2292
  await incrementSkillSuccess(
1700
2293
  String(row.assigned_to),
1701
2294
  row.project_name ? String(row.project_name) : null
1702
2295
  );
1703
- } catch {
2296
+ } catch (err) {
2297
+ process.stderr.write(
2298
+ `[tasks-crud] incrementSkillSuccess failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}
2299
+ `
2300
+ );
1704
2301
  }
1705
2302
  }
1706
2303
  if (input.status === "done" || input.status === "needs_review" || input.status === "cancelled" || input.status === "closed") {
1707
2304
  try {
1708
- const { clearQueueForAgent } = await import("./intercom-queue-6M5IRX3Y.js");
1709
2305
  clearQueueForAgent(String(row.assigned_to));
1710
- } catch {
2306
+ } catch (err) {
2307
+ process.stderr.write(
2308
+ `[tasks-crud] clearQueueForAgent failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}
2309
+ `
2310
+ );
1711
2311
  }
1712
2312
  }
1713
2313
  try {
@@ -1720,6 +2320,55 @@ ${input.result ?? ""}`;
1720
2320
  }
1721
2321
  return { row, taskFile, now, taskId, previousStatus, requestedStatus };
1722
2322
  }
2323
+ async function reopenTask(taskId, reason) {
2324
+ const client = getClient();
2325
+ const row = await resolveTask(client, taskId);
2326
+ const id = String(row.id);
2327
+ const previousStatus = String(row.status);
2328
+ if (previousStatus === "open") {
2329
+ return { taskId: id, previousStatus, reopened: false };
2330
+ }
2331
+ const validNext = VALID_TRANSITIONS[previousStatus];
2332
+ if (validNext && !validNext.includes("open")) {
2333
+ throw new Error(
2334
+ `Cannot reopen task ${id}: ${previousStatus} \u2192 open is not a valid transition. Valid transitions from "${previousStatus}": [${validNext.join(", ")}]`
2335
+ );
2336
+ }
2337
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2338
+ const stalePrefix = `STALE (auto-reopened: ${reason})
2339
+
2340
+ `;
2341
+ await client.execute({
2342
+ sql: `UPDATE tasks
2343
+ SET status = 'open',
2344
+ result = CASE
2345
+ WHEN result IS NOT NULL AND result != '' AND result NOT LIKE 'STALE (auto-reopened%'
2346
+ THEN ? || result
2347
+ ELSE result
2348
+ END,
2349
+ assigned_tmux = NULL, claimed_by = NULL, claimed_at = NULL,
2350
+ last_heartbeat_at = NULL, lease_expires_at = NULL,
2351
+ updated_at = ?
2352
+ WHERE id = ?`,
2353
+ args: [stalePrefix, now, id]
2354
+ });
2355
+ process.stderr.write(
2356
+ `[tasks-crud] Reopened task ${id} (${previousStatus} \u2192 open): ${reason}
2357
+ `
2358
+ );
2359
+ recordOrchestrationEventBestEffort({
2360
+ eventType: "task.reopened",
2361
+ source: "tasks-crud.reopenTask",
2362
+ severity: "warn",
2363
+ taskId: id,
2364
+ agentId: row.assigned_to ? String(row.assigned_to) : null,
2365
+ sessionScope: row.session_scope ? String(row.session_scope) : null,
2366
+ projectName: row.project_name ? String(row.project_name) : null,
2367
+ result: previousStatus,
2368
+ payload: { reason }
2369
+ });
2370
+ return { taskId: id, previousStatus, reopened: true };
2371
+ }
1723
2372
  async function deleteTaskCore(taskId, _baseDir) {
1724
2373
  const client = getClient();
1725
2374
  const row = await resolveTask(client, taskId);
@@ -1732,9 +2381,9 @@ async function deleteTaskCore(taskId, _baseDir) {
1732
2381
  return { taskFile, assignedTo, assignedBy, taskSlug };
1733
2382
  }
1734
2383
  async function ensureArchitectureDoc(baseDir, projectName) {
1735
- const archPath = path3.join(baseDir, "exe", "ARCHITECTURE.md");
2384
+ const archPath = path5.join(baseDir, "exe", "ARCHITECTURE.md");
1736
2385
  try {
1737
- if (existsSync3(archPath)) return;
2386
+ if (existsSync6(archPath)) return;
1738
2387
  const template = [
1739
2388
  `# ${projectName} \u2014 System Architecture`,
1740
2389
  "",
@@ -1767,10 +2416,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
1767
2416
  }
1768
2417
  }
1769
2418
  async function ensureGitignoreExe(baseDir) {
1770
- const gitignorePath = path3.join(baseDir, ".gitignore");
2419
+ const gitignorePath = path5.join(baseDir, ".gitignore");
1771
2420
  try {
1772
- if (existsSync3(gitignorePath)) {
1773
- const content = readFileSync2(gitignorePath, "utf-8");
2421
+ if (existsSync6(gitignorePath)) {
2422
+ const content = readFileSync5(gitignorePath, "utf-8");
1774
2423
  if (/^\/?exe\/?$/m.test(content)) return;
1775
2424
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
1776
2425
  } else {
@@ -1781,8 +2430,8 @@ async function ensureGitignoreExe(baseDir) {
1781
2430
  }
1782
2431
  async function cleanOrphanedTaskFiles() {
1783
2432
  const { readdir, unlink, stat } = await import("fs/promises");
1784
- const EXE_TASKS_DIR = path3.join(os3.homedir(), ".exe-os", "tasks");
1785
- if (!existsSync3(EXE_TASKS_DIR)) return 0;
2433
+ const EXE_TASKS_DIR = path5.join(os5.homedir(), ".exe-os", "tasks");
2434
+ if (!existsSync6(EXE_TASKS_DIR)) return 0;
1786
2435
  const client = getClient();
1787
2436
  let cleaned = 0;
1788
2437
  async function walkDir(dir) {
@@ -1793,11 +2442,11 @@ async function cleanOrphanedTaskFiles() {
1793
2442
  return;
1794
2443
  }
1795
2444
  for (const entry of entries) {
1796
- const fullPath = path3.join(dir, entry.name);
2445
+ const fullPath = path5.join(dir, entry.name);
1797
2446
  if (entry.isDirectory()) {
1798
2447
  await walkDir(fullPath);
1799
2448
  } else if (entry.name.endsWith(".md")) {
1800
- const relativePath = path3.relative(path3.join(os3.homedir(), ".exe-os"), fullPath);
2449
+ const relativePath = path5.relative(path5.join(os5.homedir(), ".exe-os"), fullPath);
1801
2450
  try {
1802
2451
  const result = await client.execute({
1803
2452
  sql: "SELECT id FROM tasks WHERE task_file = ? LIMIT 1",
@@ -1860,7 +2509,7 @@ async function readLatestCheckpoint(agentId) {
1860
2509
  }
1861
2510
  async function createTaskGroup(input) {
1862
2511
  const client = getClient();
1863
- const id = crypto.randomUUID();
2512
+ const id = crypto2.randomUUID();
1864
2513
  const now = (/* @__PURE__ */ new Date()).toISOString();
1865
2514
  await client.execute({
1866
2515
  sql: `INSERT INTO task_groups (id, title, coordinator, session_scope, project_name, timeout_minutes, on_partial_failure, created_at, updated_at)
@@ -2155,8 +2804,8 @@ async function checkTaskFileConsistency() {
2155
2804
  for (const row of result.rows) {
2156
2805
  const taskFile = String(row.task_file ?? "");
2157
2806
  if (!taskFile) continue;
2158
- const fullPath = taskFile.startsWith("/") ? taskFile : path3.join(os3.homedir(), ".exe-os", taskFile);
2159
- if (existsSync3(fullPath)) continue;
2807
+ const fullPath = taskFile.startsWith("/") ? taskFile : path5.join(os5.homedir(), ".exe-os", taskFile);
2808
+ if (existsSync6(fullPath)) continue;
2160
2809
  let repaired = false;
2161
2810
  try {
2162
2811
  const mdContent = renderTaskMarkdown({
@@ -2169,271 +2818,41 @@ async function checkTaskFileConsistency() {
2169
2818
  projectName: String(row.project_name ?? ""),
2170
2819
  created: String(row.created_at ?? "").split("T")[0] || "",
2171
2820
  reviewer: row.reviewer ? String(row.reviewer) : row.assigned_by ? String(row.assigned_by) : "",
2172
- context: String(row.context ?? ""),
2173
- parentTaskId: row.parent_task_id ? String(row.parent_task_id) : null
2174
- });
2175
- await writeTaskMarkdownAtomic(fullPath, mdContent);
2176
- repaired = true;
2177
- } catch {
2178
- }
2179
- if (repaired) {
2180
- recordOrchestrationEventBestEffort({
2181
- eventType: "consistency.repaired",
2182
- source: "tasks-crud.checkTaskFileConsistency",
2183
- severity: "info",
2184
- taskId: String(row.id),
2185
- agentId: row.assigned_to ? String(row.assigned_to) : null,
2186
- sessionScope: row.session_scope ? String(row.session_scope) : null,
2187
- projectName: row.project_name ? String(row.project_name) : null,
2188
- payload: { taskFile, status: String(row.status), type: "db_row_missing_file_regenerated" }
2189
- });
2190
- } else {
2191
- mismatches++;
2192
- recordOrchestrationEventBestEffort({
2193
- eventType: "consistency.mismatch",
2194
- source: "tasks-crud.checkTaskFileConsistency",
2195
- severity: "warn",
2196
- taskId: String(row.id),
2197
- agentId: row.assigned_to ? String(row.assigned_to) : null,
2198
- sessionScope: row.session_scope ? String(row.session_scope) : null,
2199
- projectName: row.project_name ? String(row.project_name) : null,
2200
- payload: { taskFile, status: String(row.status), type: "db_row_missing_file" }
2201
- });
2202
- }
2203
- }
2204
- } catch {
2205
- }
2206
- return mismatches;
2207
- }
2208
-
2209
- // src/lib/notifications.ts
2210
- import crypto2 from "crypto";
2211
- import path4 from "path";
2212
- import os4 from "os";
2213
- import {
2214
- readFileSync as readFileSync3,
2215
- readdirSync,
2216
- unlinkSync,
2217
- existsSync as existsSync4,
2218
- rmdirSync
2219
- } from "fs";
2220
- var CLEANUP_DAYS = 7;
2221
- async function writeNotification(notification) {
2222
- try {
2223
- const client = getClient();
2224
- const id = crypto2.randomUUID();
2225
- const now = (/* @__PURE__ */ new Date()).toISOString();
2226
- const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
2227
- await client.execute({
2228
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
2229
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2230
- args: [
2231
- id,
2232
- notification.agentId,
2233
- notification.agentRole,
2234
- notification.event,
2235
- notification.project,
2236
- notification.summary,
2237
- notification.taskFile ?? null,
2238
- sessionScope,
2239
- now
2240
- ]
2241
- });
2242
- } catch (err) {
2243
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
2244
- `);
2245
- }
2246
- }
2247
- async function readUnreadNotifications(agentFilter, sessionScope) {
2248
- try {
2249
- const client = getClient();
2250
- const conditions = ["read = 0"];
2251
- const args = [];
2252
- const scope = strictSessionScopeFilter(sessionScope ?? void 0);
2253
- if (agentFilter) {
2254
- conditions.push("agent_id = ?");
2255
- args.push(agentFilter);
2256
- }
2257
- const result = await client.execute({
2258
- sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
2259
- FROM notifications
2260
- WHERE ${conditions.join(" AND ")}${scope.sql}
2261
- ORDER BY created_at ASC`,
2262
- args: [...args, ...scope.args]
2263
- });
2264
- return result.rows.map((r) => ({
2265
- id: String(r.id),
2266
- agentId: String(r.agent_id),
2267
- agentRole: String(r.agent_role),
2268
- event: String(r.event),
2269
- project: String(r.project),
2270
- summary: String(r.summary),
2271
- taskFile: r.task_file ? String(r.task_file) : void 0,
2272
- sessionScope: r.session_scope == null ? null : String(r.session_scope),
2273
- timestamp: String(r.created_at),
2274
- read: false
2275
- }));
2276
- } catch {
2277
- return [];
2278
- }
2279
- }
2280
- async function markAsRead(ids, sessionScope) {
2281
- if (ids.length === 0) return;
2282
- try {
2283
- const client = getClient();
2284
- const placeholders = ids.map(() => "?").join(", ");
2285
- const scope = strictSessionScopeFilter(sessionScope ?? void 0);
2286
- await client.execute({
2287
- sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
2288
- args: [...ids, ...scope.args]
2289
- });
2290
- } catch {
2291
- }
2292
- }
2293
- async function markAsReadByTaskFile(taskFile, sessionScope) {
2294
- try {
2295
- const client = getClient();
2296
- const scope = strictSessionScopeFilter(sessionScope);
2297
- await client.execute({
2298
- sql: `UPDATE notifications SET read = 1
2299
- WHERE task_file = ? AND read = 0${scope.sql}`,
2300
- args: [taskFile, ...scope.args]
2301
- });
2302
- } catch {
2303
- }
2304
- }
2305
- async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
2306
- try {
2307
- const client = getClient();
2308
- const cutoff = new Date(
2309
- Date.now() - daysOld * 24 * 60 * 60 * 1e3
2310
- ).toISOString();
2311
- const scope = strictSessionScopeFilter(sessionScope ?? void 0);
2312
- const result = await client.execute({
2313
- sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
2314
- args: [cutoff, ...scope.args]
2315
- });
2316
- return result.rowsAffected;
2317
- } catch {
2318
- return 0;
2319
- }
2320
- }
2321
- async function markDoneTaskNotificationsAsRead(sessionScope) {
2322
- try {
2323
- const client = getClient();
2324
- const scope = strictSessionScopeFilter(sessionScope ?? void 0);
2325
- const result = await client.execute({
2326
- sql: `UPDATE notifications SET read = 1
2327
- WHERE read = 0
2328
- AND task_file IS NOT NULL
2329
- ${scope.sql}
2330
- AND task_file IN (
2331
- SELECT task_file FROM tasks WHERE status IN ('done', 'closed', 'cancelled')
2332
- )`,
2333
- args: [...scope.args]
2334
- });
2335
- return result.rowsAffected;
2336
- } catch {
2337
- return 0;
2338
- }
2339
- }
2340
- function formatNotifications(notifications) {
2341
- if (notifications.length === 0) return "";
2342
- const grouped = /* @__PURE__ */ new Map();
2343
- for (const n of notifications) {
2344
- const key = `${n.agentId}|${n.agentRole}`;
2345
- if (!grouped.has(key)) grouped.set(key, []);
2346
- grouped.get(key).push(n);
2347
- }
2348
- const lines = [];
2349
- lines.push(`## Notifications (${notifications.length} unread)
2350
- `);
2351
- for (const [key, items] of grouped) {
2352
- const [agentId, agentRole] = key.split("|");
2353
- lines.push(`**${agentId}** (${agentRole}):`);
2354
- for (const item of items) {
2355
- const ago = formatTimeAgo(item.timestamp);
2356
- const icon = eventIcon(item.event);
2357
- lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
2358
- }
2359
- lines.push("");
2360
- }
2361
- return lines.join("\n");
2362
- }
2363
- async function migrateJsonNotifications() {
2364
- const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path4.join(os4.homedir(), ".exe-os");
2365
- const notifDir = path4.join(base, "notifications");
2366
- if (!existsSync4(notifDir)) return 0;
2367
- let migrated = 0;
2368
- try {
2369
- const files = readdirSync(notifDir).filter((f) => f.endsWith(".json"));
2370
- if (files.length === 0) return 0;
2371
- const client = getClient();
2372
- for (const file of files) {
2373
- try {
2374
- const filePath = path4.join(notifDir, file);
2375
- const data = JSON.parse(readFileSync3(filePath, "utf8"));
2376
- await client.execute({
2377
- sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
2378
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2379
- args: [
2380
- crypto2.randomUUID(),
2381
- data.agentId ?? "unknown",
2382
- data.agentRole ?? "unknown",
2383
- data.event ?? "session_summary",
2384
- data.project ?? "unknown",
2385
- data.summary ?? "",
2386
- data.taskFile ?? null,
2387
- null,
2388
- data.read ? 1 : 0,
2389
- data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
2390
- ]
2821
+ context: String(row.context ?? ""),
2822
+ parentTaskId: row.parent_task_id ? String(row.parent_task_id) : null
2391
2823
  });
2392
- unlinkSync(filePath);
2393
- migrated++;
2824
+ await writeTaskMarkdownAtomic(fullPath, mdContent);
2825
+ repaired = true;
2394
2826
  } catch {
2395
2827
  }
2396
- }
2397
- try {
2398
- const remaining = readdirSync(notifDir);
2399
- if (remaining.length === 0) {
2400
- rmdirSync(notifDir);
2828
+ if (repaired) {
2829
+ recordOrchestrationEventBestEffort({
2830
+ eventType: "consistency.repaired",
2831
+ source: "tasks-crud.checkTaskFileConsistency",
2832
+ severity: "info",
2833
+ taskId: String(row.id),
2834
+ agentId: row.assigned_to ? String(row.assigned_to) : null,
2835
+ sessionScope: row.session_scope ? String(row.session_scope) : null,
2836
+ projectName: row.project_name ? String(row.project_name) : null,
2837
+ payload: { taskFile, status: String(row.status), type: "db_row_missing_file_regenerated" }
2838
+ });
2839
+ } else {
2840
+ mismatches++;
2841
+ recordOrchestrationEventBestEffort({
2842
+ eventType: "consistency.mismatch",
2843
+ source: "tasks-crud.checkTaskFileConsistency",
2844
+ severity: "warn",
2845
+ taskId: String(row.id),
2846
+ agentId: row.assigned_to ? String(row.assigned_to) : null,
2847
+ sessionScope: row.session_scope ? String(row.session_scope) : null,
2848
+ projectName: row.project_name ? String(row.project_name) : null,
2849
+ payload: { taskFile, status: String(row.status), type: "db_row_missing_file" }
2850
+ });
2401
2851
  }
2402
- } catch {
2403
2852
  }
2404
2853
  } catch {
2405
2854
  }
2406
- return migrated;
2407
- }
2408
- function eventIcon(event) {
2409
- switch (event) {
2410
- case "task_complete":
2411
- return "Completed:";
2412
- case "task_needs_fix":
2413
- return "Needs fix:";
2414
- case "session_summary":
2415
- return "Session:";
2416
- case "error_spike":
2417
- return "Errors:";
2418
- case "orphan_task":
2419
- return "Orphan:";
2420
- case "subtasks_complete":
2421
- return "Subtasks done:";
2422
- case "task_group_barrier":
2423
- return "Task group:";
2424
- case "capacity_relaunch":
2425
- return "Relaunched:";
2426
- }
2427
- }
2428
- function formatTimeAgo(timestamp) {
2429
- const diffMs = Date.now() - new Date(timestamp).getTime();
2430
- const mins = Math.floor(diffMs / 6e4);
2431
- if (mins < 1) return "just now";
2432
- if (mins < 60) return `${mins}m ago`;
2433
- const hours = Math.floor(mins / 60);
2434
- if (hours < 24) return `${hours}h ago`;
2435
- const days = Math.floor(hours / 24);
2436
- return `${days}d ago`;
2855
+ return mismatches;
2437
2856
  }
2438
2857
 
2439
2858
  // src/lib/tasks-review.ts
@@ -2448,9 +2867,11 @@ function formatAge(isoTimestamp) {
2448
2867
  const days = Math.floor(hours / 24);
2449
2868
  return `${days}d ago`;
2450
2869
  }
2870
+ var STALE_REVIEW_AGE_MS = 24 * 60 * 60 * 1e3;
2871
+ var SIGNAL_FILE_TTL_MS = 48 * 60 * 60 * 1e3;
2451
2872
  function isStale(isoTimestamp) {
2452
2873
  if (!isoTimestamp) return false;
2453
- return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
2874
+ return Date.now() - new Date(isoTimestamp).getTime() > STALE_REVIEW_AGE_MS;
2454
2875
  }
2455
2876
  async function countPendingReviews(sessionScope, reviewer) {
2456
2877
  const extraConditions = reviewer ? ["reviewer = ?"] : void 0;
@@ -2505,43 +2926,197 @@ async function listPendingReviews(limit, sessionScope, projectName, reviewer) {
2505
2926
  });
2506
2927
  return rows;
2507
2928
  }
2929
+ var ORPHAN_REVIEW_COLUMNS = "id, task_file, session_scope, reviewer, assigned_by, assigned_to, title, project_name";
2930
+ function toOrphanedReviewRow(r) {
2931
+ const str = (v) => v == null ? null : String(v);
2932
+ return {
2933
+ id: String(r.id),
2934
+ task_file: str(r.task_file),
2935
+ session_scope: str(r.session_scope),
2936
+ reviewer: str(r.reviewer),
2937
+ assigned_by: str(r.assigned_by),
2938
+ assigned_to: str(r.assigned_to),
2939
+ title: str(r.title),
2940
+ project_name: str(r.project_name)
2941
+ };
2942
+ }
2943
+ function errMsg(err) {
2944
+ return err instanceof Error ? err.message : String(err);
2945
+ }
2946
+ async function invalidateReviewArtifacts(row) {
2947
+ if (row.task_file) {
2948
+ try {
2949
+ await markAsReadByTaskFile(row.task_file, row.session_scope ?? null);
2950
+ } catch (err) {
2951
+ process.stderr.write(`[cleanup] notification invalidation failed for task ${row.id}: ${errMsg(err)}
2952
+ `);
2953
+ }
2954
+ }
2955
+ try {
2956
+ const dir = taskSignalDir(row.session_scope || "default");
2957
+ if (existsSync7(dir)) {
2958
+ for (const f of readdirSync2(dir)) {
2959
+ if (f.endsWith(`.review-${row.id}.pending`)) {
2960
+ unlinkSync3(path6.join(dir, f));
2961
+ }
2962
+ }
2963
+ }
2964
+ } catch (err) {
2965
+ process.stderr.write(`[cleanup] signal-file invalidation failed for task ${row.id}: ${errMsg(err)}
2966
+ `);
2967
+ }
2968
+ try {
2969
+ clearCompletionNudge(row.id);
2970
+ clearStaleReviewEscalation(row.id);
2971
+ } catch (err) {
2972
+ process.stderr.write(`[cleanup] dedup invalidation failed for task ${row.id}: ${errMsg(err)}
2973
+ `);
2974
+ }
2975
+ try {
2976
+ const scope = row.session_scope;
2977
+ if (scope) {
2978
+ const targets = [scope];
2979
+ const reviewer = row.reviewer || row.assigned_by;
2980
+ if (reviewer && !isCoordinatorName(reviewer)) {
2981
+ try {
2982
+ targets.push(employeeSessionName(reviewer, scope));
2983
+ } catch {
2984
+ }
2985
+ }
2986
+ for (const target of targets) {
2987
+ drainForSession(target, (_session, item) => item.intercomReason === "completion");
2988
+ }
2989
+ }
2990
+ } catch (err) {
2991
+ process.stderr.write(`[cleanup] intercom-queue invalidation failed for task ${row.id}: ${errMsg(err)}
2992
+ `);
2993
+ }
2994
+ }
2995
+ async function closeReviewRows(client, rows, status, now) {
2996
+ if (rows.length === 0) return 0;
2997
+ const placeholders = rows.map(() => "?").join(", ");
2998
+ const result = await client.execute({
2999
+ sql: `UPDATE tasks SET status = ?, updated_at = ? WHERE id IN (${placeholders})`,
3000
+ args: [status, now, ...rows.map((r) => r.id)]
3001
+ });
3002
+ for (const row of rows) {
3003
+ await invalidateReviewArtifacts(row);
3004
+ }
3005
+ return result.rowsAffected;
3006
+ }
3007
+ function sweepExpiredSignalFiles(now = Date.now()) {
3008
+ let deleted = 0;
3009
+ try {
3010
+ const base = path6.join(homedir2(), ".exe-os", "task-signals");
3011
+ if (!existsSync7(base)) return 0;
3012
+ for (const scopeName of readdirSync2(base)) {
3013
+ const scopeDir = path6.join(base, scopeName);
3014
+ let entries;
3015
+ try {
3016
+ entries = readdirSync2(scopeDir);
3017
+ } catch {
3018
+ continue;
3019
+ }
3020
+ for (const f of entries) {
3021
+ if (!f.endsWith(".pending")) continue;
3022
+ const filePath = path6.join(scopeDir, f);
3023
+ try {
3024
+ if (now - statSync3(filePath).mtimeMs > SIGNAL_FILE_TTL_MS) {
3025
+ unlinkSync3(filePath);
3026
+ deleted++;
3027
+ }
3028
+ } catch {
3029
+ }
3030
+ }
3031
+ }
3032
+ } catch (err) {
3033
+ process.stderr.write(`[cleanup] signal-file TTL sweep failed: ${errMsg(err)}
3034
+ `);
3035
+ }
3036
+ if (deleted > 0) {
3037
+ process.stderr.write(`[cleanup] Deleted ${deleted} expired signal file(s) (>48h old)
3038
+ `);
3039
+ }
3040
+ return deleted;
3041
+ }
2508
3042
  async function cleanupOrphanedReviews(sessionScope) {
2509
3043
  const client = getClient();
2510
3044
  const now = (/* @__PURE__ */ new Date()).toISOString();
2511
3045
  const scope = sessionScopeFilter(sessionScope);
2512
- const r1 = await client.execute({
2513
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3046
+ const p1 = await client.execute({
3047
+ sql: `SELECT ${ORPHAN_REVIEW_COLUMNS} FROM tasks
2514
3048
  WHERE status IN ('open', 'needs_review', 'in_progress')
2515
3049
  AND assigned_by = 'system'
2516
3050
  AND title LIKE 'Review:%'
2517
3051
  AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))
2518
3052
  ${scope.sql}`,
2519
- args: [now, ...scope.args]
3053
+ args: [...scope.args]
2520
3054
  });
2521
- const r1b = await client.execute({
2522
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3055
+ const cascadeRows = p1.rows.map((r) => toOrphanedReviewRow(r));
3056
+ const cascadeClosed = await closeReviewRows(client, cascadeRows, "cancelled", now);
3057
+ const p2 = await client.execute({
3058
+ sql: `SELECT ${ORPHAN_REVIEW_COLUMNS} FROM tasks
2523
3059
  WHERE status IN ('open', 'needs_review')
2524
3060
  AND title LIKE 'Review:%completed%'
2525
3061
  AND (parent_task_id IS NULL OR parent_task_id NOT IN (SELECT id FROM tasks WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')))
2526
3062
  ${scope.sql}`,
2527
- args: [now, ...scope.args]
3063
+ args: [...scope.args]
3064
+ });
3065
+ const orphanRows = p2.rows.map((r) => toOrphanedReviewRow(r));
3066
+ const orphanClosed = await closeReviewRows(client, orphanRows, "cancelled", now);
3067
+ const p3 = await client.execute({
3068
+ sql: `SELECT ${ORPHAN_REVIEW_COLUMNS} FROM tasks
3069
+ WHERE status = 'needs_review'
3070
+ AND result IS NOT NULL
3071
+ AND parent_task_id IS NOT NULL
3072
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))
3073
+ ${scope.sql}`,
3074
+ args: [...scope.args]
2528
3075
  });
2529
- const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
2530
- const r2 = await client.execute({
2531
- sql: `UPDATE tasks SET status = 'closed', updated_at = ?
3076
+ const staleOrphanRows = p3.rows.map((r) => toOrphanedReviewRow(r));
3077
+ const staleClosed = await closeReviewRows(client, staleOrphanRows, "closed", now);
3078
+ const staleThreshold = new Date(Date.now() - STALE_REVIEW_AGE_MS).toISOString();
3079
+ const p4 = await client.execute({
3080
+ sql: `SELECT ${ORPHAN_REVIEW_COLUMNS} FROM tasks
2532
3081
  WHERE status = 'needs_review'
2533
3082
  AND result IS NOT NULL
2534
3083
  AND updated_at < ?
2535
3084
  ${scope.sql}`,
2536
- args: [now, staleThreshold, ...scope.args]
3085
+ args: [staleThreshold, ...scope.args]
2537
3086
  });
2538
- const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
3087
+ let escalated = 0;
3088
+ if (p4.rows.length > 0) {
3089
+ const employees = await loadEmployees();
3090
+ for (const r of p4.rows) {
3091
+ const row = toOrphanedReviewRow(r);
3092
+ if (!claimStaleReviewEscalation(row.id)) continue;
3093
+ const reviewer = row.reviewer || row.assigned_by || getCoordinatorName(employees);
3094
+ await writeNotification({
3095
+ agentId: reviewer,
3096
+ agentRole: isCoordinatorName(reviewer, employees) ? "COO" : "manager",
3097
+ event: "orphan_task",
3098
+ project: row.project_name ?? "",
3099
+ summary: `STALE REVIEW: "${row.title ?? row.id}" by ${row.assigned_to ?? "unknown"} has been awaiting review for over 24h \u2014 review and close it (auto-close is disabled).`,
3100
+ taskFile: row.task_file ?? void 0,
3101
+ sessionScope: row.session_scope
3102
+ });
3103
+ escalated++;
3104
+ }
3105
+ }
3106
+ if (escalated > 0) {
3107
+ process.stderr.write(
3108
+ `[cleanup] Escalated ${escalated} stale review(s) (>24h in needs_review, awaiting reviewer)
3109
+ `
3110
+ );
3111
+ }
3112
+ const total = cascadeClosed + orphanClosed + staleClosed;
2539
3113
  if (total > 0) {
2540
3114
  process.stderr.write(
2541
- `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
3115
+ `[cleanup] Closed ${total} orphaned review(s): ${cascadeClosed} cascade + ${orphanClosed} orphan + ${staleClosed} stale
2542
3116
  `
2543
3117
  );
2544
3118
  }
3119
+ sweepExpiredSignalFiles();
2545
3120
  return total;
2546
3121
  }
2547
3122
  function getReviewChecklist(role, agent, taskSlug) {
@@ -2671,19 +3246,21 @@ async function createReviewForCompletedTask(row, result, _baseDir, now) {
2671
3246
  const autoApprove = false;
2672
3247
  if (!autoApprove) {
2673
3248
  try {
2674
- const { claimCompletionNudge } = await import("./completion-nudge-dedup-DBZF24MM.js");
2675
3249
  if (claimCompletionNudge(originalTaskId)) {
2676
3250
  const taskScope = row.session_scope ? String(row.session_scope) : null;
2677
3251
  const exeSession = taskScope || getParentExe(getSessionKey());
2678
3252
  if (exeSession && isCoordinatorName(reviewer, employees)) {
2679
3253
  sendIntercom(exeSession, { force: true, reason: "completion" });
2680
3254
  } else if (exeSession && reviewer) {
2681
- const { employeeSessionName: employeeSessionName2 } = await import("./lib/tmux-routing.js");
2682
- const reviewerSession = employeeSessionName2(reviewer, exeSession);
3255
+ const reviewerSession = employeeSessionName(reviewer, exeSession);
2683
3256
  sendIntercom(reviewerSession, { force: true, reason: "completion" });
2684
3257
  }
2685
3258
  }
2686
- } catch {
3259
+ } catch (err) {
3260
+ process.stderr.write(
3261
+ `[review] completion intercom failed for "${taskTitle}": ${err instanceof Error ? err.message : String(err)}
3262
+ `
3263
+ );
2687
3264
  }
2688
3265
  }
2689
3266
  if (autoApprove) {
@@ -2753,11 +3330,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2753
3330
  );
2754
3331
  }
2755
3332
  try {
2756
- const cacheDir = path5.join(EXE_AI_DIR, "session-cache");
2757
- if (existsSync5(cacheDir)) {
3333
+ const cacheDir = path6.join(EXE_AI_DIR, "session-cache");
3334
+ if (existsSync7(cacheDir)) {
2758
3335
  for (const f of readdirSync2(cacheDir)) {
2759
3336
  if (f.startsWith("review-notified-")) {
2760
- unlinkSync2(path5.join(cacheDir, f));
3337
+ unlinkSync3(path6.join(cacheDir, f));
2761
3338
  }
2762
3339
  }
2763
3340
  }
@@ -2767,8 +3344,8 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2767
3344
 
2768
3345
  // src/lib/worktree-guard.ts
2769
3346
  import { execFileSync } from "child_process";
2770
- import { existsSync as existsSync6 } from "fs";
2771
- import path6 from "path";
3347
+ import { existsSync as existsSync8 } from "fs";
3348
+ import path7 from "path";
2772
3349
  var GIT_TIMEOUT_MS = 1e4;
2773
3350
  function listWorktreesPorcelain(repoRoot) {
2774
3351
  try {
@@ -2795,7 +3372,7 @@ function listWorktreesPorcelain(repoRoot) {
2795
3372
  }
2796
3373
  }
2797
3374
  function inferSessionOwner(wtPath, branch) {
2798
- const dirName = path6.basename(wtPath);
3375
+ const dirName = path7.basename(wtPath);
2799
3376
  if (dirName && dirName !== ".worktrees") {
2800
3377
  return dirName;
2801
3378
  }
@@ -2826,7 +3403,7 @@ function checkWorktreeCollisions(repoPath) {
2826
3403
  const worktrees = listWorktreesPorcelain(repoRoot);
2827
3404
  for (const wt of worktrees) {
2828
3405
  if (!wt.path.includes("/.worktrees/")) continue;
2829
- if (!existsSync6(wt.path)) continue;
3406
+ if (!existsSync8(wt.path)) continue;
2830
3407
  const owner = inferSessionOwner(wt.path, wt.branch);
2831
3408
  const dirty = isWorktreeDirty(wt.path);
2832
3409
  const hasCommits = hasUnpushedCommits(wt.path, wt.branch);
@@ -2871,13 +3448,13 @@ function formatIntercomPrompt(reason = "nudge") {
2871
3448
  var _cachedExeBinDir;
2872
3449
  function resolveExeBinDir() {
2873
3450
  if (_cachedExeBinDir !== void 0) return _cachedExeBinDir;
2874
- const nodeBinDir = path7.dirname(process.execPath);
2875
- if (existsSync7(path7.join(nodeBinDir, "exe-launch-agent"))) {
3451
+ const nodeBinDir = path8.dirname(process.execPath);
3452
+ if (existsSync9(path8.join(nodeBinDir, "exe-launch-agent"))) {
2876
3453
  _cachedExeBinDir = nodeBinDir;
2877
3454
  return _cachedExeBinDir;
2878
3455
  }
2879
- const localBin = path7.join(os5.homedir(), ".local", "bin");
2880
- if (existsSync7(path7.join(localBin, "exe-launch-agent"))) {
3456
+ const localBin = path8.join(os6.homedir(), ".local", "bin");
3457
+ if (existsSync9(path8.join(localBin, "exe-launch-agent"))) {
2881
3458
  _cachedExeBinDir = localBin;
2882
3459
  return _cachedExeBinDir;
2883
3460
  }
@@ -2888,9 +3465,9 @@ function _resetExeBinDirCache() {
2888
3465
  _cachedExeBinDir = void 0;
2889
3466
  }
2890
3467
  function buildSpawnPath(inheritedPath = process.env.PATH, execPath = process.execPath) {
2891
- const sep = path7.delimiter;
2892
- const nodeBinDir = path7.dirname(execPath);
2893
- const localBin = path7.join(os5.homedir(), ".local", "bin");
3468
+ const sep = path8.delimiter;
3469
+ const nodeBinDir = path8.dirname(execPath);
3470
+ const localBin = path8.join(os6.homedir(), ".local", "bin");
2894
3471
  const exeBinDir = resolveExeBinDir();
2895
3472
  const prefixDirs = [nodeBinDir, exeBinDir, localBin].filter(
2896
3473
  (d) => Boolean(d)
@@ -2907,9 +3484,9 @@ function buildSpawnPath(inheritedPath = process.env.PATH, execPath = process.exe
2907
3484
  }
2908
3485
  return ordered.join(sep);
2909
3486
  }
2910
- var SPAWN_LOCK_DIR = path7.join(os5.homedir(), ".exe-os", "spawn-locks");
3487
+ var SPAWN_LOCK_DIR = path8.join(os6.homedir(), ".exe-os", "spawn-locks");
2911
3488
  function spawnLockPath(sessionName) {
2912
- return path7.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3489
+ return path8.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
2913
3490
  }
2914
3491
  function isProcessAlive(pid) {
2915
3492
  try {
@@ -2920,15 +3497,15 @@ function isProcessAlive(pid) {
2920
3497
  }
2921
3498
  }
2922
3499
  function acquireSpawnLock(sessionName) {
2923
- if (!existsSync7(SPAWN_LOCK_DIR)) {
2924
- mkdirSync3(SPAWN_LOCK_DIR, { recursive: true });
3500
+ if (!existsSync9(SPAWN_LOCK_DIR)) {
3501
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
2925
3502
  }
2926
3503
  const lockFile = spawnLockPath(sessionName);
2927
3504
  const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
2928
3505
  try {
2929
- const fd = openSync(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
2930
- writeSync(fd, lockData);
2931
- closeSync(fd);
3506
+ const fd = openSync2(lockFile, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL, 420);
3507
+ writeSync2(fd, lockData);
3508
+ closeSync2(fd);
2932
3509
  return true;
2933
3510
  } catch (err) {
2934
3511
  if (err?.code !== "EEXIST") {
@@ -2936,19 +3513,19 @@ function acquireSpawnLock(sessionName) {
2936
3513
  }
2937
3514
  }
2938
3515
  try {
2939
- const lock = JSON.parse(readFileSync4(lockFile, "utf8"));
3516
+ const lock = JSON.parse(readFileSync6(lockFile, "utf8"));
2940
3517
  const age = Date.now() - lock.timestamp;
2941
3518
  if (isProcessAlive(lock.pid) && age < 6e4) {
2942
3519
  return false;
2943
3520
  }
2944
3521
  const tmpName = `${lockFile}.stale-${process.pid}-${Date.now()}`;
2945
3522
  try {
2946
- renameSync2(lockFile, tmpName);
3523
+ renameSync3(lockFile, tmpName);
2947
3524
  try {
2948
- unlinkSync3(tmpName);
3525
+ unlinkSync4(tmpName);
2949
3526
  } catch {
2950
3527
  }
2951
- writeFileSync2(lockFile, lockData);
3528
+ writeFileSync5(lockFile, lockData);
2952
3529
  return true;
2953
3530
  } catch {
2954
3531
  return false;
@@ -2956,12 +3533,12 @@ function acquireSpawnLock(sessionName) {
2956
3533
  } catch {
2957
3534
  const tmpName = `${lockFile}.stale-${process.pid}-${Date.now()}`;
2958
3535
  try {
2959
- renameSync2(lockFile, tmpName);
3536
+ renameSync3(lockFile, tmpName);
2960
3537
  try {
2961
- unlinkSync3(tmpName);
3538
+ unlinkSync4(tmpName);
2962
3539
  } catch {
2963
3540
  }
2964
- writeFileSync2(lockFile, lockData);
3541
+ writeFileSync5(lockFile, lockData);
2965
3542
  return true;
2966
3543
  } catch {
2967
3544
  return false;
@@ -2970,7 +3547,7 @@ function acquireSpawnLock(sessionName) {
2970
3547
  }
2971
3548
  function releaseSpawnLock(sessionName) {
2972
3549
  try {
2973
- unlinkSync3(spawnLockPath(sessionName));
3550
+ unlinkSync4(spawnLockPath(sessionName));
2974
3551
  } catch {
2975
3552
  }
2976
3553
  }
@@ -2994,18 +3571,18 @@ function reconcileStaleSpawnLock(sessionName, isAlive = isEmployeeAlive) {
2994
3571
  });
2995
3572
  return { skip: false, brokeStaleLock: true };
2996
3573
  }
2997
- var SESSION_CACHE = path7.join(os5.homedir(), ".exe-os", "session-cache");
3574
+ var SESSION_CACHE = path8.join(os6.homedir(), ".exe-os", "session-cache");
2998
3575
  var BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
2999
3576
  function resolveBehaviorsExporterScript() {
3000
3577
  try {
3001
3578
  const thisFile = fileURLToPath(import.meta.url);
3002
- const scriptPath = path7.join(
3003
- path7.dirname(thisFile),
3579
+ const scriptPath = path8.join(
3580
+ path8.dirname(thisFile),
3004
3581
  "..",
3005
3582
  "bin",
3006
3583
  "exe-export-behaviors.js"
3007
3584
  );
3008
- return existsSync7(scriptPath) ? scriptPath : null;
3585
+ return existsSync9(scriptPath) ? scriptPath : null;
3009
3586
  } catch {
3010
3587
  return null;
3011
3588
  }
@@ -3109,11 +3686,11 @@ function extractRootExe(name) {
3109
3686
  return tail.length > 0 ? tail : null;
3110
3687
  }
3111
3688
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3112
- if (!existsSync7(SESSION_CACHE)) {
3113
- mkdirSync3(SESSION_CACHE, { recursive: true });
3689
+ if (!existsSync9(SESSION_CACHE)) {
3690
+ mkdirSync6(SESSION_CACHE, { recursive: true });
3114
3691
  }
3115
3692
  const rootExe = extractRootExe(parentExe) ?? parentExe;
3116
- const filePath = path7.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3693
+ const filePath = path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3117
3694
  atomicWriteJsonSync(filePath, {
3118
3695
  parentExe: rootExe,
3119
3696
  dispatchedBy: dispatchedBy || rootExe,
@@ -3123,7 +3700,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3123
3700
  var PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
3124
3701
  function getParentExe(sessionKey) {
3125
3702
  try {
3126
- const data = JSON.parse(readFileSync4(path7.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3703
+ const data = JSON.parse(readFileSync6(path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3127
3704
  if (data.registeredAt) {
3128
3705
  const age = Date.now() - new Date(data.registeredAt).getTime();
3129
3706
  if (age > PARENT_EXE_CACHE_TTL_MS) return null;
@@ -3135,8 +3712,8 @@ function getParentExe(sessionKey) {
3135
3712
  }
3136
3713
  function getDispatchedBy(sessionKey) {
3137
3714
  try {
3138
- const data = JSON.parse(readFileSync4(
3139
- path7.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3715
+ const data = JSON.parse(readFileSync6(
3716
+ path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3140
3717
  "utf8"
3141
3718
  ));
3142
3719
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -3357,7 +3934,7 @@ async function verifyPaneAtCapacity(sessionName) {
3357
3934
  reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
3358
3935
  };
3359
3936
  }
3360
- const { isAtCapacity } = await import("./capacity-monitor-WAEHVBL7.js");
3937
+ const { isAtCapacity } = await import("./capacity-monitor-6O3PWYNM.js");
3361
3938
  if (!isAtCapacity(pane)) {
3362
3939
  return {
3363
3940
  atCapacity: false,
@@ -3371,15 +3948,15 @@ async function verifyPaneAtCapacity(sessionName) {
3371
3948
  }
3372
3949
  var INTERCOM_DEBOUNCE_MS = 3e5;
3373
3950
  var CODEX_DEBOUNCE_MS = 3e5;
3374
- var INTERCOM_LOG = path7.join(os5.homedir(), ".exe-os", "intercom.log");
3375
- var INTERCOM_LOG_MAX_BYTES = 1024 * 1024;
3951
+ var INTERCOM_LOG2 = path8.join(os6.homedir(), ".exe-os", "intercom.log");
3952
+ var INTERCOM_LOG_MAX_BYTES2 = 1024 * 1024;
3376
3953
  var execFileAsync = promisify(execFile);
3377
- var DEBOUNCE_FILE = path7.join(SESSION_CACHE, "intercom-debounce.json");
3954
+ var DEBOUNCE_FILE = path8.join(SESSION_CACHE, "intercom-debounce.json");
3378
3955
  var DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3379
3956
  function readDebounceState() {
3380
3957
  try {
3381
- if (!existsSync7(DEBOUNCE_FILE)) return {};
3382
- const raw = JSON.parse(readFileSync4(DEBOUNCE_FILE, "utf8"));
3958
+ if (!existsSync9(DEBOUNCE_FILE)) return {};
3959
+ const raw = JSON.parse(readFileSync6(DEBOUNCE_FILE, "utf8"));
3383
3960
  const state = {};
3384
3961
  for (const [key, val] of Object.entries(raw)) {
3385
3962
  if (typeof val === "number") {
@@ -3395,7 +3972,7 @@ function readDebounceState() {
3395
3972
  }
3396
3973
  function writeDebounceState(state) {
3397
3974
  try {
3398
- if (!existsSync7(SESSION_CACHE)) mkdirSync3(SESSION_CACHE, { recursive: true });
3975
+ if (!existsSync9(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3399
3976
  atomicWriteJsonSync(DEBOUNCE_FILE, state);
3400
3977
  } catch {
3401
3978
  }
@@ -3403,9 +3980,9 @@ function writeDebounceState(state) {
3403
3980
  function resolveSessionRuntime(targetSession) {
3404
3981
  const agentName = baseAgentName(targetSession.split("-")[0] ?? "");
3405
3982
  try {
3406
- const diPath = path7.join(SESSION_CACHE, `dispatch-info-${targetSession}.json`);
3407
- if (existsSync7(diPath)) {
3408
- const di = JSON.parse(readFileSync4(diPath, "utf-8"));
3983
+ const diPath = path8.join(SESSION_CACHE, `dispatch-info-${targetSession}.json`);
3984
+ if (existsSync9(diPath)) {
3985
+ const di = JSON.parse(readFileSync6(diPath, "utf-8"));
3409
3986
  if (di.runtime) return di.runtime;
3410
3987
  }
3411
3988
  return getAgentRuntime(agentName).runtime;
@@ -3445,10 +4022,10 @@ function logIntercom(msg, meta) {
3445
4022
  process.stderr.write(`[intercom] ${msg}${metaStr}
3446
4023
  `);
3447
4024
  try {
3448
- if (existsSync7(INTERCOM_LOG) && statSync2(INTERCOM_LOG).size > INTERCOM_LOG_MAX_BYTES) {
3449
- renameSync2(INTERCOM_LOG, `${INTERCOM_LOG}.1`);
4025
+ if (existsSync9(INTERCOM_LOG2) && statSync4(INTERCOM_LOG2).size > INTERCOM_LOG_MAX_BYTES2) {
4026
+ renameSync3(INTERCOM_LOG2, `${INTERCOM_LOG2}.1`);
3450
4027
  }
3451
- appendFileSync2(INTERCOM_LOG, line);
4028
+ appendFileSync3(INTERCOM_LOG2, line);
3452
4029
  } catch {
3453
4030
  }
3454
4031
  }
@@ -3479,6 +4056,59 @@ function isExeSession(sessionName) {
3479
4056
  const coordinatorName = getCoordinatorName();
3480
4057
  return matchesBaseWithInstance(coordinatorName);
3481
4058
  }
4059
+ var PROMPT_INPUT_LINE = /(?:^|\s|│)\s*[❯›>]\s?(.*)$/u;
4060
+ var PROMPT_PLACEHOLDER = /^Try\s+["'“]/u;
4061
+ function paneHasPendingInput(pane) {
4062
+ const lines = pane.split("\n");
4063
+ const tail = lines.slice(-12);
4064
+ for (let i = tail.length - 1; i >= 0; i--) {
4065
+ const line = tail[i];
4066
+ if (!line || line.trim().length === 0) continue;
4067
+ const m = PROMPT_INPUT_LINE.exec(line);
4068
+ if (!m) continue;
4069
+ const content = (m[1] ?? "").replace(/[│]\s*$/u, "").trim();
4070
+ if (content.length === 0) return false;
4071
+ if (PROMPT_PLACEHOLDER.test(content)) return false;
4072
+ return true;
4073
+ }
4074
+ return false;
4075
+ }
4076
+ function isBlockedCodexPrompt(pane) {
4077
+ const text = pane.toLowerCase();
4078
+ if (!text.includes("codex")) return false;
4079
+ const asksForChoice = /\(y\/n\)|\[y\/n\]|yes\/no|press enter|select .*model|choose .*model/.test(text);
4080
+ const modelPrompt = /model.*(upgrade|switch|newer|recommended|select|choose)|upgrade.*model|switch.*model/.test(text);
4081
+ return asksForChoice && modelPrompt;
4082
+ }
4083
+ function sessionHasPendingInput(sessionName) {
4084
+ try {
4085
+ const transport = getTransport();
4086
+ return paneHasPendingInput(transport.capturePane(sessionName, 12));
4087
+ } catch {
4088
+ return false;
4089
+ }
4090
+ }
4091
+ function decideQueuedNudgeDrain(intercomReason, state) {
4092
+ if (state.hasInProgressTask) {
4093
+ return { action: "drop", reason: "in_progress_task_will_auto_chain" };
4094
+ }
4095
+ if (intercomReason === "message" && !state.hasUnreadMessages) {
4096
+ return { action: "drop", reason: "inbox_empty" };
4097
+ }
4098
+ if ((intercomReason === "completion" || intercomReason === "notification") && state.pendingReviewCount === 0) {
4099
+ return { action: "drop", reason: "no_pending_reviews" };
4100
+ }
4101
+ return { action: "context", context: formatIntercomPrompt(intercomReason ?? "nudge") };
4102
+ }
4103
+ async function countScopedPendingReviews(targetSession, reviewer) {
4104
+ try {
4105
+ const scope = extractRootExe(targetSession);
4106
+ const sessionScope = scope && isExeSession(scope) ? scope : void 0;
4107
+ return await countPendingReviews(sessionScope, reviewer);
4108
+ } catch {
4109
+ return null;
4110
+ }
4111
+ }
3482
4112
  function sendIntercom(targetSession, opts) {
3483
4113
  const transport = getTransport();
3484
4114
  const startedAt = Date.now();
@@ -3561,6 +4191,13 @@ function sendIntercom(targetSession, opts) {
3561
4191
  recordResult("failed", "session_not_found");
3562
4192
  return "failed";
3563
4193
  }
4194
+ if (isExeSession(targetSession) && reason !== "message") {
4195
+ queueIntercom(targetSession, "coordinator session \u2014 nudges deferred to prompt-submit hook", reason);
4196
+ const batched2 = recordDebounce(targetSession);
4197
+ logIntercom(`QUEUED \u2192 ${targetSession} (coordinator \u2014 nudges are never keystroke-injected)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
4198
+ recordResult("queued", "coordinator_no_inject");
4199
+ return "queued";
4200
+ }
3564
4201
  const sessionState = getSessionState(targetSession);
3565
4202
  if (sessionState === "no_claude") {
3566
4203
  queueIntercom(targetSession, "claude not running in session", reason);
@@ -3580,8 +4217,8 @@ function sendIntercom(targetSession, opts) {
3580
4217
  try {
3581
4218
  const rawAgent2 = targetSession.split("-")[0] ?? targetSession;
3582
4219
  const agent = baseAgentName(rawAgent2);
3583
- const markerPath = path7.join(SESSION_CACHE, `current-task-${agent}.json`);
3584
- if (existsSync7(markerPath)) {
4220
+ const markerPath = path8.join(SESSION_CACHE, `current-task-${agent}.json`);
4221
+ if (existsSync9(markerPath)) {
3585
4222
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
3586
4223
  recordResult("debounced", "in_progress_marker");
3587
4224
  return "debounced";
@@ -3593,6 +4230,13 @@ function sendIntercom(targetSession, opts) {
3593
4230
  logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
3594
4231
  transport.sendKeys(targetSession, "q");
3595
4232
  }
4233
+ if (sessionHasPendingInput(targetSession)) {
4234
+ queueIntercom(targetSession, "non-empty input buffer at send time", reason);
4235
+ const batched2 = recordDebounce(targetSession);
4236
+ logIntercom(`QUEUED \u2192 ${targetSession} (non-empty input buffer)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
4237
+ recordResult("queued", "pending_input");
4238
+ return "queued";
4239
+ }
3596
4240
  const msg = formatIntercomPrompt(reason);
3597
4241
  transport.sendKeysLiteral(targetSession, msg, resolveSessionRuntime(targetSession));
3598
4242
  const batched = recordDebounce(targetSession);
@@ -3606,6 +4250,67 @@ function sendIntercom(targetSession, opts) {
3606
4250
  return "failed";
3607
4251
  }
3608
4252
  }
4253
+ var DEFERRED_SPAWN_NUDGE_ATTEMPTS = 3;
4254
+ var DEFERRED_SPAWN_NUDGE_RETRY_MS = Number(process.env.EXE_DEFERRED_SPAWN_NUDGE_RETRY_MS || 5e3);
4255
+ function scheduleDeferredSpawnNudge(input) {
4256
+ const attemptSend = (attempt) => {
4257
+ setTimeout(() => {
4258
+ try {
4259
+ if (!input.transport.isAlive(input.sessionName)) {
4260
+ throw new Error("session not alive at deferred nudge time");
4261
+ }
4262
+ input.transport.sendKeysLiteral(input.sessionName, input.message, input.runtime);
4263
+ process.stderr.write(
4264
+ `[tmux-routing] deferred spawn nudge delivered to ${input.sessionName} (attempt ${attempt}/${DEFERRED_SPAWN_NUDGE_ATTEMPTS})
4265
+ `
4266
+ );
4267
+ recordOrchestrationEventBestEffort({
4268
+ eventType: "dispatch.spawn_nudge.delivered",
4269
+ source: "tmux-routing.scheduleDeferredSpawnNudge",
4270
+ agentId: input.employeeName,
4271
+ sessionScope: input.exeSession,
4272
+ tmuxSession: input.sessionName,
4273
+ runtime: input.runtime,
4274
+ result: "delivered",
4275
+ payload: { attempt }
4276
+ });
4277
+ } catch (err) {
4278
+ const msg = err instanceof Error ? err.message : String(err);
4279
+ process.stderr.write(
4280
+ `[tmux-routing] deferred spawn nudge failed for ${input.sessionName} (attempt ${attempt}/${DEFERRED_SPAWN_NUDGE_ATTEMPTS}): ${msg}
4281
+ `
4282
+ );
4283
+ recordOrchestrationEventBestEffort({
4284
+ eventType: "dispatch.spawn_nudge.failed",
4285
+ source: "tmux-routing.scheduleDeferredSpawnNudge",
4286
+ severity: "warn",
4287
+ agentId: input.employeeName,
4288
+ sessionScope: input.exeSession,
4289
+ tmuxSession: input.sessionName,
4290
+ runtime: input.runtime,
4291
+ result: "failed",
4292
+ errorCode: "deferred_nudge_failed",
4293
+ payload: { attempt, error: msg }
4294
+ });
4295
+ if (attempt < DEFERRED_SPAWN_NUDGE_ATTEMPTS) {
4296
+ attemptSend(attempt + 1);
4297
+ return;
4298
+ }
4299
+ queueIntercom(input.sessionName, `spawn nudge failed after ${attempt} attempts: ${msg}`, "signal");
4300
+ void import("./notifications-N5NKUYAU.js").then(({ writeNotification: writeNotification2 }) => writeNotification2({
4301
+ agentId: getCoordinatorName(),
4302
+ agentRole: "COO",
4303
+ event: "error_spike",
4304
+ project: "",
4305
+ summary: `Spawned ${input.sessionName}, but the initial task nudge failed after ${attempt} attempts (${msg}). Worker may be idle with open task; check session and re-dispatch if needed.`,
4306
+ sessionScope: input.exeSession
4307
+ })).catch(() => {
4308
+ });
4309
+ }
4310
+ }, attempt === 1 ? 1e4 : DEFERRED_SPAWN_NUDGE_RETRY_MS);
4311
+ };
4312
+ attemptSend(1);
4313
+ }
3609
4314
  var CODEX_SUBMIT_DELAY_MS = Number(process.env.EXE_CODEX_SUBMIT_DELAY_MS || 250);
3610
4315
  var SUBMIT_DELAY_MS = Number(process.env.EXE_SUBMIT_DELAY_MS || 60);
3611
4316
  async function sendKeysLiteralAsync(targetSession, text, runtime) {
@@ -3615,17 +4320,17 @@ async function sendKeysLiteralAsync(targetSession, text, runtime) {
3615
4320
  await execFileAsync("tmux", ["send-keys", "-t", targetSession, isCodex ? "C-m" : "Enter"], { timeout: 1e3 });
3616
4321
  };
3617
4322
  if (text.length >= 500) {
3618
- const tmpDir = path7.join(os5.tmpdir(), "exe-os-intercom");
3619
- mkdirSync3(tmpDir, { recursive: true });
3620
- const tmpFile = path7.join(tmpDir, `msg-${process.pid}-${Date.now()}.txt`);
4323
+ const tmpDir = path8.join(os6.tmpdir(), "exe-os-intercom");
4324
+ mkdirSync6(tmpDir, { recursive: true });
4325
+ const tmpFile = path8.join(tmpDir, `msg-${process.pid}-${Date.now()}.txt`);
3621
4326
  try {
3622
- writeFileSync2(tmpFile, text, "utf-8");
4327
+ writeFileSync5(tmpFile, text, "utf-8");
3623
4328
  await execFileAsync("tmux", ["load-buffer", tmpFile], { timeout: 3e3 });
3624
4329
  await execFileAsync("tmux", ["paste-buffer", "-p", "-t", targetSession], { timeout: 3e3 });
3625
4330
  await submit();
3626
4331
  } finally {
3627
4332
  try {
3628
- unlinkSync3(tmpFile);
4333
+ unlinkSync4(tmpFile);
3629
4334
  } catch {
3630
4335
  }
3631
4336
  }
@@ -3695,6 +4400,13 @@ async function sendIntercomAsync(targetSession, opts) {
3695
4400
  recordResult("failed", "session_not_found");
3696
4401
  return "failed";
3697
4402
  }
4403
+ if (isExeSession(targetSession) && reason !== "message") {
4404
+ queueIntercom(targetSession, "coordinator session \u2014 nudges deferred to prompt-submit hook", reason);
4405
+ const batched2 = recordDebounce(targetSession);
4406
+ logIntercom(`QUEUED \u2192 ${targetSession} (coordinator \u2014 nudges are never keystroke-injected)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
4407
+ recordResult("queued", "coordinator_no_inject");
4408
+ return "queued";
4409
+ }
3698
4410
  const sessionState = getSessionState(targetSession);
3699
4411
  if (sessionState === "no_claude") {
3700
4412
  queueIntercom(targetSession, "claude not running in session", reason);
@@ -3718,6 +4430,13 @@ async function sendIntercomAsync(targetSession, opts) {
3718
4430
  }
3719
4431
  } catch {
3720
4432
  }
4433
+ if (sessionHasPendingInput(targetSession)) {
4434
+ queueIntercom(targetSession, "non-empty input buffer at send time", reason);
4435
+ const batched2 = recordDebounce(targetSession);
4436
+ logIntercom(`QUEUED \u2192 ${targetSession} (non-empty input buffer)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
4437
+ recordResult("queued", "pending_input");
4438
+ return "queued";
4439
+ }
3721
4440
  await sendKeysLiteralAsync(targetSession, formatIntercomPrompt(reason), resolveSessionRuntime(targetSession));
3722
4441
  const batched = recordDebounce(targetSession);
3723
4442
  logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (async fire-and-forget)`);
@@ -3788,10 +4507,10 @@ async function ensureWorktreeAsync(projectDir, employeeName, instance) {
3788
4507
  const repoRoot = rootOut.trim();
3789
4508
  if (!repoRoot) return null;
3790
4509
  const label = instance != null && instance > 0 ? `${employeeName}${instance}` : employeeName;
3791
- const wtPath = path7.join(repoRoot, ".worktrees", label);
4510
+ const wtPath = path8.join(repoRoot, ".worktrees", label);
3792
4511
  const branch = `${label}-work`;
3793
- if (existsSync7(path7.join(wtPath, ".git"))) return wtPath;
3794
- mkdirSync3(path7.join(repoRoot, ".worktrees"), { recursive: true });
4512
+ if (existsSync9(path8.join(wtPath, ".git"))) return wtPath;
4513
+ mkdirSync6(path8.join(repoRoot, ".worktrees"), { recursive: true });
3795
4514
  try {
3796
4515
  await execFileAsync("git", ["worktree", "prune"], { cwd: repoRoot, timeout: 1e4 });
3797
4516
  } catch {
@@ -3854,6 +4573,21 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3854
4573
  error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
3855
4574
  };
3856
4575
  }
4576
+ {
4577
+ const roster = loadEmployeesSync();
4578
+ if (roster.length > 0) {
4579
+ const base = baseAgentName(employeeName, roster);
4580
+ const known = roster.some((e) => e.name.toLowerCase() === base.toLowerCase());
4581
+ if (!known) {
4582
+ const names = roster.map((e) => e.name).sort().join(", ");
4583
+ return {
4584
+ status: "failed",
4585
+ sessionName: "",
4586
+ error: `Unknown employee "${employeeName}" \u2014 not in the roster. Refusing to spawn a non-roster agent (task ids, typos, and stale names are rejected here). Known employees: ${names}. (bug 98476a80)`
4587
+ };
4588
+ }
4589
+ }
4590
+ }
3857
4591
  if (!isRootSession(exeSession)) {
3858
4592
  const root = extractRootExe(exeSession);
3859
4593
  if (root) {
@@ -4075,7 +4809,7 @@ async function ensureEmployeeAsync(employeeName, exeSession, projectDir, opts) {
4075
4809
  }
4076
4810
  try {
4077
4811
  const sessionName = employeeSessionName(employeeName, exeSession, asyncOpts.instance);
4078
- await exportBehaviorsAsync(employeeName, path7.basename(asyncOpts.cwd ?? projectDir), sessionName);
4812
+ await exportBehaviorsAsync(employeeName, path8.basename(asyncOpts.cwd ?? projectDir), sessionName);
4079
4813
  } catch (err) {
4080
4814
  process.stderr.write(
4081
4815
  `[ensureEmployeeAsync] behavior export failed for ${employeeName}: ${err instanceof Error ? err.message : String(err)}
@@ -4093,8 +4827,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4093
4827
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4094
4828
  const spawnStartedAt = Date.now();
4095
4829
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4096
- const logDir = path7.join(os5.homedir(), ".exe-os", "session-logs");
4097
- const logFile = path7.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4830
+ const logDir = path8.join(os6.homedir(), ".exe-os", "session-logs");
4831
+ const logFile = path8.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4098
4832
  {
4099
4833
  const earlyRt = resolveSpawnRuntimeConfig(
4100
4834
  employeeName,
@@ -4130,24 +4864,24 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4130
4864
  runtime: opts?.runtimeOverride ?? null,
4131
4865
  payload: { hasCwdOverride: Boolean(opts?.cwd), autoInstance: Boolean(opts?.autoInstance) }
4132
4866
  });
4133
- if (!existsSync7(logDir)) {
4134
- mkdirSync3(logDir, { recursive: true });
4867
+ if (!existsSync9(logDir)) {
4868
+ mkdirSync6(logDir, { recursive: true });
4135
4869
  }
4136
4870
  transport.kill(sessionName, exeSession);
4137
4871
  let cleanupSuffix = "";
4138
4872
  try {
4139
4873
  const thisFile = fileURLToPath(import.meta.url);
4140
- const cleanupScript = path7.join(path7.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4141
- if (existsSync7(cleanupScript)) {
4874
+ const cleanupScript = path8.join(path8.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4875
+ if (existsSync9(cleanupScript)) {
4142
4876
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
4143
4877
  }
4144
4878
  } catch {
4145
4879
  }
4146
4880
  try {
4147
- const claudeJsonPath = path7.join(os5.homedir(), ".claude.json");
4881
+ const claudeJsonPath = path8.join(os6.homedir(), ".claude.json");
4148
4882
  let claudeJson = {};
4149
4883
  try {
4150
- claudeJson = JSON.parse(readFileSync4(claudeJsonPath, "utf8"));
4884
+ claudeJson = JSON.parse(readFileSync6(claudeJsonPath, "utf8"));
4151
4885
  } catch {
4152
4886
  }
4153
4887
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -4159,13 +4893,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4159
4893
  } catch {
4160
4894
  }
4161
4895
  try {
4162
- const settingsDir = path7.join(os5.homedir(), ".claude", "projects");
4896
+ const settingsDir = path8.join(os6.homedir(), ".claude", "projects");
4163
4897
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4164
- const projSettingsDir = path7.join(settingsDir, normalizedKey);
4165
- const settingsPath = path7.join(projSettingsDir, "settings.json");
4898
+ const projSettingsDir = path8.join(settingsDir, normalizedKey);
4899
+ const settingsPath = path8.join(projSettingsDir, "settings.json");
4166
4900
  let settings = {};
4167
4901
  try {
4168
- settings = JSON.parse(readFileSync4(settingsPath, "utf8"));
4902
+ settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
4169
4903
  } catch {
4170
4904
  }
4171
4905
  const perms = settings.permissions ?? {};
@@ -4193,7 +4927,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4193
4927
  if (changed) {
4194
4928
  perms.allow = allow;
4195
4929
  settings.permissions = perms;
4196
- mkdirSync3(projSettingsDir, { recursive: true });
4930
+ mkdirSync6(projSettingsDir, { recursive: true });
4197
4931
  atomicWriteSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4198
4932
  }
4199
4933
  } catch {
@@ -4251,8 +4985,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4251
4985
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
4252
4986
  let legacyFallbackWarned = false;
4253
4987
  if (!useExeAgent && !useBinSymlink) {
4254
- const identityPath = path7.join(
4255
- os5.homedir(),
4988
+ const identityPath = path8.join(
4989
+ os6.homedir(),
4256
4990
  ".exe-os",
4257
4991
  "identity",
4258
4992
  `${employeeName}.md`
@@ -4267,13 +5001,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4267
5001
  `
4268
5002
  );
4269
5003
  }
4270
- } else if (existsSync7(identityPath)) {
5004
+ } else if (existsSync9(identityPath)) {
4271
5005
  legacyFallbackWarned = true;
4272
5006
  }
4273
5007
  if (!opts?.behaviorsExported) {
4274
5008
  const behaviorsFile = exportBehaviorsSync(
4275
5009
  employeeName,
4276
- path7.basename(spawnCwd),
5010
+ path8.basename(spawnCwd),
4277
5011
  sessionName
4278
5012
  );
4279
5013
  void behaviorsFile;
@@ -4286,9 +5020,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4286
5020
  );
4287
5021
  }
4288
5022
  try {
4289
- const ctxDir = path7.join(os5.homedir(), ".exe-os", "session-cache");
4290
- mkdirSync3(ctxDir, { recursive: true });
4291
- const ctxFile = path7.join(ctxDir, `session-context-${sessionName}.md`);
5023
+ const ctxDir = path8.join(os6.homedir(), ".exe-os", "session-cache");
5024
+ mkdirSync6(ctxDir, { recursive: true });
5025
+ const ctxFile = path8.join(ctxDir, `session-context-${sessionName}.md`);
4292
5026
  const ctxContent = [
4293
5027
  `## Session Context`,
4294
5028
  `You are running in tmux session: ${sessionName}.`,
@@ -4379,8 +5113,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4379
5113
  } else {
4380
5114
  let launcherAbs = "";
4381
5115
  try {
4382
- const launcherJs = path7.join(path7.dirname(fileURLToPath(import.meta.url)), "..", "bin", "exe-launch-agent.js");
4383
- if (existsSync7(launcherJs)) launcherAbs = launcherJs;
5116
+ const launcherJs = path8.join(path8.dirname(fileURLToPath(import.meta.url)), "..", "bin", "exe-launch-agent.js");
5117
+ if (existsSync9(launcherJs)) launcherAbs = launcherJs;
4384
5118
  } catch {
4385
5119
  }
4386
5120
  spawnCommand = launcherAbs ? `"${process.execPath}" "${launcherAbs}" --agent ${employeeName}${cleanupSuffix}` : `exe-launch-agent --agent ${employeeName}${cleanupSuffix}`;
@@ -4426,7 +5160,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4426
5160
  transport.pipeLog(sessionName, logFile);
4427
5161
  try {
4428
5162
  const mySession = getMySession();
4429
- const dispatchInfo = path7.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5163
+ const dispatchInfo = path8.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4430
5164
  atomicWriteJsonSync(dispatchInfo, {
4431
5165
  dispatchedBy: mySession,
4432
5166
  rootExe: exeSession,
@@ -4468,8 +5202,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4468
5202
  } else {
4469
5203
  spawnFailureCause = "timeout";
4470
5204
  }
4471
- const errMsg = `spawn did not start: tmux session "${sessionName}" never came alive within ${LIVENESS_CAP_MS / 1e3}s (${runtimeLabel} process failed to launch or exited immediately; cause=${spawnFailureCause}). Check ~/.exe-os/session-logs/${instanceLabel}-*.log \u2014 common cause: installed Claude Code does not support a launch flag, or the daemon PATH excludes the node/launcher bin dir (cause=path_not_found).`;
4472
- process.stderr.write(`[tmux-routing] SPAWN FAILED ${sessionName} (${spawnFailureCause}): ${errMsg}
5205
+ const errMsg2 = `spawn did not start: tmux session "${sessionName}" never came alive within ${LIVENESS_CAP_MS / 1e3}s (${runtimeLabel} process failed to launch or exited immediately; cause=${spawnFailureCause}). Check ~/.exe-os/session-logs/${instanceLabel}-*.log \u2014 common cause: installed Claude Code does not support a launch flag, or the daemon PATH excludes the node/launcher bin dir (cause=path_not_found).`;
5206
+ process.stderr.write(`[tmux-routing] SPAWN FAILED ${sessionName} (${spawnFailureCause}): ${errMsg2}
4473
5207
  `);
4474
5208
  recordOrchestrationEventBestEffort({
4475
5209
  eventType: "tmux.spawn.completed",
@@ -4487,7 +5221,39 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4487
5221
  errorCode: spawnFailureCause,
4488
5222
  payload: { spawnFailureCause, detail: "spawn_session_not_live", spawnCommand }
4489
5223
  });
4490
- return { sessionName, error: errMsg };
5224
+ return { sessionName, error: errMsg2 };
5225
+ }
5226
+ if (useCodex) {
5227
+ let pane = "";
5228
+ try {
5229
+ pane = transport.capturePane(sessionName, 80);
5230
+ } catch {
5231
+ }
5232
+ if (isBlockedCodexPrompt(pane)) {
5233
+ try {
5234
+ execFileSync2("tmux", ["kill-session", "-t", sessionName], { timeout: 3e3 });
5235
+ } catch {
5236
+ }
5237
+ releaseSpawnLock(sessionName);
5238
+ const errMsg2 = `codex spawn blocked by an interactive model prompt in ${sessionName}; killed session so it can respawn non-interactively`;
5239
+ process.stderr.write(`[tmux-routing] SPAWN FAILED ${sessionName} (codex_blocked_prompt): ${errMsg2}
5240
+ `);
5241
+ recordOrchestrationEventBestEffort({
5242
+ eventType: "tmux.spawn.completed",
5243
+ source: "tmux-routing.spawnEmployee",
5244
+ severity: "warn",
5245
+ agentId: employeeName,
5246
+ sessionScope: exeSession,
5247
+ instanceId: opts?.instance != null ? String(opts.instance) : null,
5248
+ tmuxSession: sessionName,
5249
+ runtime: "codex",
5250
+ durationMs: Date.now() - spawnStartedAt,
5251
+ result: "failed",
5252
+ errorCode: "codex_blocked_prompt",
5253
+ payload: { detail: "interactive_model_prompt" }
5254
+ });
5255
+ return { sessionName, error: errMsg2 };
5256
+ }
4491
5257
  }
4492
5258
  const POLL_CAP_MS = callerIsDaemon ? 0 : 45e3;
4493
5259
  const pollIterations = Math.ceil(POLL_CAP_MS / POLL_INTERVAL_MS);
@@ -4533,21 +5299,18 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4533
5299
  exeSession,
4534
5300
  dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
4535
5301
  };
4536
- const cachePath = path7.join(os5.homedir(), ".exe-os", "session-cache", `boot-cache-${sessionName}.json`);
4537
- writeFileSync2(cachePath, JSON.stringify(bootCache));
5302
+ const cachePath = path8.join(os6.homedir(), ".exe-os", "session-cache", `boot-cache-${sessionName}.json`);
5303
+ writeFileSync5(cachePath, JSON.stringify(bootCache));
4538
5304
  } catch {
4539
5305
  }
4540
- const _transport = transport;
4541
- const _session = sessionName;
4542
- const _emp = employeeName;
4543
- setTimeout(() => {
4544
- try {
4545
- _transport.sendKeysLiteral(_session, `You have pending tasks. Run list_tasks and start your highest priority task immediately.`);
4546
- } catch (e) {
4547
- process.stderr.write(`[tmux-routing] deferred nudge ${_emp}: ${e.message}
4548
- `);
4549
- }
4550
- }, 1e4);
5306
+ scheduleDeferredSpawnNudge({
5307
+ sessionName,
5308
+ employeeName,
5309
+ exeSession,
5310
+ runtime: useOpencode ? "opencode" : "claude",
5311
+ transport,
5312
+ message: `You have pending tasks. Run list_tasks and start your highest priority task immediately.`
5313
+ });
4551
5314
  } else {
4552
5315
  try {
4553
5316
  transport.sendKeysLiteral(sessionName, `You have pending tasks. Run list_tasks and start immediately.`);
@@ -4598,6 +5361,105 @@ function isAmbiguousMultiCoordinatorContext() {
4598
5361
  return false;
4599
5362
  }
4600
5363
  }
5364
+ var MULTI_COORDINATOR_CACHE_TTL_MS = 5e3;
5365
+ var _multiCoordinatorCache = null;
5366
+ function normalizeProjectDir(dir) {
5367
+ if (!dir || typeof dir !== "string") return "";
5368
+ return dir.replace(/\/+$/, "");
5369
+ }
5370
+ function isMultiCoordinatorSharedProject(opts) {
5371
+ if (process.env.EXE_STRICT_SESSION_SCOPE === "1") return true;
5372
+ if (process.env.EXE_STRICT_SESSION_SCOPE === "0") return false;
5373
+ const injected = opts?.sessions !== void 0;
5374
+ if (!injected) {
5375
+ if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") return false;
5376
+ const now = Date.now();
5377
+ if (_multiCoordinatorCache && now - _multiCoordinatorCache.at < MULTI_COORDINATOR_CACHE_TTL_MS) {
5378
+ return _multiCoordinatorCache.value;
5379
+ }
5380
+ }
5381
+ let value = false;
5382
+ try {
5383
+ const sessions = opts?.sessions ?? listSessions();
5384
+ let liveTmuxSet = null;
5385
+ if (opts && "liveTmux" in opts) {
5386
+ liveTmuxSet = opts.liveTmux ? new Set(opts.liveTmux) : null;
5387
+ } else if (!injected) {
5388
+ try {
5389
+ liveTmuxSet = new Set(getTransport().listSessions());
5390
+ } catch {
5391
+ liveTmuxSet = null;
5392
+ }
5393
+ }
5394
+ const roots = sessions.filter(
5395
+ (s) => s.windowName && !s.windowName.includes("-") && (!liveTmuxSet || liveTmuxSet.has(s.windowName))
5396
+ );
5397
+ outer: for (let i = 0; i < roots.length; i++) {
5398
+ const a = normalizeProjectDir(roots[i].projectDir);
5399
+ if (!a) continue;
5400
+ for (let j = i + 1; j < roots.length; j++) {
5401
+ if (roots[i].windowName === roots[j].windowName) continue;
5402
+ const b = normalizeProjectDir(roots[j].projectDir);
5403
+ if (!b) continue;
5404
+ if (a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`)) {
5405
+ value = true;
5406
+ break outer;
5407
+ }
5408
+ }
5409
+ }
5410
+ } catch {
5411
+ value = false;
5412
+ }
5413
+ if (!injected) _multiCoordinatorCache = { at: Date.now(), value };
5414
+ return value;
5415
+ }
5416
+ function isScopeCoordinatorDead(scope, opts) {
5417
+ if (!isValidSessionScope(scope)) return false;
5418
+ const injected = opts?.sessions !== void 0 || opts !== void 0 && "liveTmux" in opts;
5419
+ if (!injected && (process.env.VITEST === "true" || process.env.NODE_ENV === "test")) {
5420
+ return false;
5421
+ }
5422
+ let registryReadable = false;
5423
+ try {
5424
+ const sessions = opts?.sessions ?? listSessions();
5425
+ registryReadable = true;
5426
+ if (sessions.some((s) => s.windowName === scope)) return false;
5427
+ } catch {
5428
+ registryReadable = false;
5429
+ }
5430
+ let tmuxReadable = false;
5431
+ try {
5432
+ const live = opts && "liveTmux" in opts ? opts.liveTmux ?? [] : getTransport().listSessions();
5433
+ tmuxReadable = opts && "liveTmux" in opts ? opts.liveTmux !== null : true;
5434
+ if (live.includes(scope)) return false;
5435
+ } catch {
5436
+ tmuxReadable = false;
5437
+ }
5438
+ return registryReadable || tmuxReadable;
5439
+ }
5440
+ function allowDeadScopeRescue(taskId, rowScope, currentScope, opts) {
5441
+ if (!isScopeCoordinatorDead(rowScope, opts)) return false;
5442
+ process.stderr.write(
5443
+ `[dead-scope rescue] task ${taskId} (scope ${rowScope}, dead) mutated by ${currentScope}
5444
+ `
5445
+ );
5446
+ void (async () => {
5447
+ try {
5448
+ const { getClient: getClient2 } = await import("./lib/database.js");
5449
+ const note = `
5450
+
5451
+ [dead-scope rescue ${(/* @__PURE__ */ new Date()).toISOString()}] Owning coordinator "${rowScope}" is dead; mutation allowed from "${currentScope}".`;
5452
+ await getClient2().execute({
5453
+ sql: `UPDATE tasks
5454
+ SET context = COALESCE(context, '') || ?, updated_at = ?
5455
+ WHERE id = ? AND (context IS NULL OR context NOT LIKE '%[dead-scope rescue%')`,
5456
+ args: [note, (/* @__PURE__ */ new Date()).toISOString(), taskId]
5457
+ });
5458
+ } catch {
5459
+ }
5460
+ })();
5461
+ return true;
5462
+ }
4601
5463
  function getCurrentSessionScope() {
4602
5464
  if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
4603
5465
  return null;
@@ -4618,7 +5480,7 @@ function getCurrentSessionScope() {
4618
5480
  }
4619
5481
  }
4620
5482
  var _scopeWarningEmitted = false;
4621
- function sessionScopeFilter(sessionScope, tableAlias) {
5483
+ function sessionScopeFilter(sessionScope, tableAlias, opts) {
4622
5484
  const autoDetect = sessionScope === void 0;
4623
5485
  const scope = autoDetect ? getCurrentSessionScope() : sessionScope;
4624
5486
  if (!scope) {
@@ -4628,6 +5490,10 @@ function sessionScopeFilter(sessionScope, tableAlias) {
4628
5490
  return { sql: "", args: [] };
4629
5491
  }
4630
5492
  const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
5493
+ const strict = opts?.strictSession ?? isMultiCoordinatorSharedProject();
5494
+ if (strict) {
5495
+ return { sql: ` AND ${col} = ?`, args: [scope] };
5496
+ }
4631
5497
  return {
4632
5498
  sql: ` AND (${col} IS NULL OR ${col} = ?)`,
4633
5499
  args: [scope]
@@ -4656,10 +5522,12 @@ export {
4656
5522
  claudeSupportsAgentFlag,
4657
5523
  PROVIDER_TABLE,
4658
5524
  DEFAULT_PROVIDER,
4659
- logTaskDispatch,
4660
5525
  isValidSessionScope,
4661
5526
  sanitizeSessionScope,
4662
5527
  isAmbiguousMultiCoordinatorContext,
5528
+ isMultiCoordinatorSharedProject,
5529
+ isScopeCoordinatorDead,
5530
+ allowDeadScopeRescue,
4663
5531
  getCurrentSessionScope,
4664
5532
  sessionScopeFilter,
4665
5533
  strictSessionScopeFilter,
@@ -4671,6 +5539,13 @@ export {
4671
5539
  markDoneTaskNotificationsAsRead,
4672
5540
  formatNotifications,
4673
5541
  migrateJsonNotifications,
5542
+ readQueue,
5543
+ queueIntercom,
5544
+ drainQueue,
5545
+ drainForSession,
5546
+ clearQueueForAgent,
5547
+ _resetDrainGuard,
5548
+ logTaskDispatch,
4674
5549
  DEFAULT_TASK_PRIORITY,
4675
5550
  normalizeTaskPriority,
4676
5551
  isStructuredTaskCompletionReport,
@@ -4678,14 +5553,17 @@ export {
4678
5553
  writeCheckpoint,
4679
5554
  extractParentFromContext,
4680
5555
  slugify,
5556
+ checkLaneAffinity,
4681
5557
  resolveTask,
4682
5558
  createTaskCore,
4683
5559
  queryTaskRows,
4684
5560
  listTasks,
4685
5561
  isTmuxSessionAlive,
4686
5562
  TASK_ALREADY_CLAIMED_PREFIX,
5563
+ VALID_TASK_STATUSES,
4687
5564
  checkStaleCompletion,
4688
5565
  updateTaskStatus,
5566
+ reopenTask,
4689
5567
  deleteTaskCore,
4690
5568
  ensureArchitectureDoc,
4691
5569
  ensureGitignoreExe,
@@ -4698,15 +5576,17 @@ export {
4698
5576
  checkAndFireBarriers,
4699
5577
  checkTaskFileConsistency,
4700
5578
  formatAge,
5579
+ STALE_REVIEW_AGE_MS,
5580
+ SIGNAL_FILE_TTL_MS,
4701
5581
  isStale,
4702
5582
  countPendingReviews,
4703
5583
  countNewPendingReviewsSince,
4704
5584
  listPendingReviews,
5585
+ sweepExpiredSignalFiles,
4705
5586
  cleanupOrphanedReviews,
4706
5587
  getReviewChecklist,
4707
5588
  createReviewForCompletedTask,
4708
5589
  cleanupReviewFile,
4709
- formatIntercomPrompt,
4710
5590
  _resetExeBinDirCache,
4711
5591
  buildSpawnPath,
4712
5592
  acquireSpawnLock,
@@ -4727,6 +5607,11 @@ export {
4727
5607
  getSessionState,
4728
5608
  isSessionBusy,
4729
5609
  isExeSession,
5610
+ paneHasPendingInput,
5611
+ isBlockedCodexPrompt,
5612
+ sessionHasPendingInput,
5613
+ decideQueuedNudgeDrain,
5614
+ countScopedPendingReviews,
4730
5615
  sendIntercom,
4731
5616
  sendIntercomAsync,
4732
5617
  notifyParentExe,