@askexenow/exe-os 0.9.271 → 0.9.273
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/active-agent-BDYXURXQ.js +26 -0
- package/dist/active-agent-YWBGAKGU.js +25 -0
- package/dist/agentic-ontology-56VHSVS3.js +25 -0
- package/dist/backfill-metadata-A3534S32.js +597 -0
- package/dist/backfill-metadata-B6F2KJJV.js +597 -0
- package/dist/backfill-metadata-BOM2MXLI.js +597 -0
- package/dist/backfill-metadata-G46ABBVR.js +597 -0
- package/dist/backfill-metadata-TAU33HJS.js +597 -0
- package/dist/backfill-metadata-VAV27KJK.js +597 -0
- package/dist/behaviors-USUTDXVA.js +25 -0
- package/dist/bin/agentic-ontology-backfill.js +5 -5
- package/dist/bin/agentic-reflection-backfill.js +6 -6
- package/dist/bin/agentic-semantic-label.js +5 -5
- package/dist/bin/backfill-conversations.js +4 -4
- package/dist/bin/backfill-responses.js +4 -4
- package/dist/bin/backfill-vectors.js +5 -5
- package/dist/bin/bulk-sync-postgres.js +6 -6
- package/dist/bin/cc-doctor.js +4 -4
- package/dist/bin/cleanup-stale-review-tasks.js +10 -10
- package/dist/bin/cli.js +16 -16
- package/dist/bin/exe-agent-config.js +3 -3
- package/dist/bin/exe-agent.js +4 -4
- package/dist/bin/exe-assign.js +5 -5
- package/dist/bin/exe-boot.js +17 -17
- package/dist/bin/exe-call.js +4 -4
- package/dist/bin/exe-cloud.js +4 -4
- package/dist/bin/exe-dispatch.js +10 -10
- package/dist/bin/exe-doctor.js +1 -1
- package/dist/bin/exe-export-behaviors.js +7 -7
- package/dist/bin/exe-forget.js +6 -6
- package/dist/bin/exe-gateway.js +7 -7
- package/dist/bin/exe-healthcheck.js +4 -4
- package/dist/bin/exe-heartbeat.js +10 -10
- package/dist/bin/exe-kill.js +13 -13
- package/dist/bin/exe-launch-agent.js +37 -19
- package/dist/bin/exe-new-employee.js +6 -6
- package/dist/bin/exe-pending-messages.js +11 -11
- package/dist/bin/exe-pending-notifications.js +33 -18
- package/dist/bin/exe-pending-reviews.js +10 -10
- package/dist/bin/exe-rename.js +4 -4
- package/dist/bin/exe-review.js +12 -12
- package/dist/bin/exe-search.js +5 -5
- package/dist/bin/exe-session-cleanup.js +15 -15
- package/dist/bin/exe-settings.js +5 -5
- package/dist/bin/exe-start-codex.js +11 -11
- package/dist/bin/exe-start-opencode.js +8 -8
- package/dist/bin/exe-status.js +11 -11
- package/dist/bin/exe-team.js +3 -3
- package/dist/bin/git-sweep.js +11 -11
- package/dist/bin/graph-backfill.js +4 -4
- package/dist/bin/graph-export.js +5 -5
- package/dist/bin/import-history.js +7 -7
- package/dist/bin/install.js +6 -6
- package/dist/bin/intercom-check.js +4 -4
- package/dist/bin/mcp-sessions.js +2 -2
- package/dist/bin/orchestration-metrics.js +4 -4
- package/dist/bin/postgres-agentic-reflection-backfill.js +2 -2
- package/dist/bin/postgres-agentic-semantic-backfill.js +1 -1
- package/dist/bin/scan-tasks.js +10 -10
- package/dist/bin/setup.js +1 -1
- package/dist/bin/shard-migrate.js +4 -4
- package/dist/capacity-monitor-2GJOFXGB.js +49 -0
- package/dist/capacity-monitor-3Z7W4K25.js +49 -0
- package/dist/capacity-monitor-BENS3N7B.js +49 -0
- package/dist/capacity-monitor-IFVRCIM7.js +49 -0
- package/dist/capacity-monitor-MQUUEZKB.js +49 -0
- package/dist/capacity-monitor-Q47GBDSY.js +49 -0
- package/dist/catchup-brief-B4KGAIPU.js +151 -0
- package/dist/catchup-brief-NMOV3SSP.js +151 -0
- package/dist/catchup-brief-RP4QHXNT.js +151 -0
- package/dist/catchup-brief-TKA6TEK4.js +151 -0
- package/dist/catchup-brief-VMF3ESTZ.js +151 -0
- package/dist/catchup-brief-ZL7V3BXC.js +151 -0
- package/dist/chunk-23KJ2LXY.js +58 -0
- package/dist/chunk-2KWVJV6I.js +171 -0
- package/dist/chunk-2NQQP3FF.js +630 -0
- package/dist/chunk-3A4SOC66.js +551 -0
- package/dist/chunk-3FU5I3KV.js +526 -0
- package/dist/chunk-3GSGDPLK.js +171 -0
- package/dist/chunk-3IM3JNQV.js +377 -0
- package/dist/chunk-3OM3V545.js +448 -0
- package/dist/chunk-3T27ZQT6.js +495 -0
- package/dist/chunk-3VI3QIHU.js +214 -0
- package/dist/chunk-3WG3RRWA.js +1345 -0
- package/dist/chunk-42A3JV3A.js +128 -0
- package/dist/chunk-46IEEKPU.js +13696 -0
- package/dist/chunk-46WLFLGP.js +1073 -0
- package/dist/chunk-4JMPQB7K.js +1148 -0
- package/dist/chunk-4L25LLQM.js +382 -0
- package/dist/chunk-4Q7X3SAM.js +204 -0
- package/dist/chunk-4UAUCFHA.js +526 -0
- package/dist/chunk-4VRJX2SP.js +495 -0
- package/dist/chunk-57RBAR2A.js +214 -0
- package/dist/chunk-57UAFTO2.js +3958 -0
- package/dist/chunk-5AS622MM.js +3958 -0
- package/dist/chunk-5JF5OQQU.js +89 -0
- package/dist/chunk-5LTY4GLX.js +13745 -0
- package/dist/chunk-5M5RYJ22.js +3955 -0
- package/dist/chunk-5YO2FER3.js +76 -0
- package/dist/chunk-62DEE65H.js +371 -0
- package/dist/chunk-62YI2JOC.js +333 -0
- package/dist/chunk-64T6DFSS.js +447 -0
- package/dist/chunk-6AGPWYFC.js +447 -0
- package/dist/chunk-6BWDP63Z.js +197 -0
- package/dist/chunk-6CH7TYBG.js +58 -0
- package/dist/chunk-6CHHFVRQ.js +284 -0
- package/dist/chunk-6D64562N.js +330 -0
- package/dist/chunk-6F35WOSR.js +447 -0
- package/dist/chunk-6GPYL7TX.js +214 -0
- package/dist/chunk-6HQ22FC6.js +81 -0
- package/dist/chunk-6KWLUVFL.js +54 -0
- package/dist/chunk-6N5ISWBF.js +1148 -0
- package/dist/chunk-6OD7PVMC.js +333 -0
- package/dist/chunk-6ZSH2BZR.js +244 -0
- package/dist/chunk-74MF4T3T.js +3962 -0
- package/dist/chunk-77H7IO3O.js +382 -0
- package/dist/chunk-7BUWNG6M.js +159 -0
- package/dist/chunk-7ET5CYTD.js +382 -0
- package/dist/chunk-7IWLKR6N.js +76 -0
- package/dist/chunk-7OEUOJL5.js +1021 -0
- package/dist/chunk-7YEQI2WF.js +13745 -0
- package/dist/chunk-AIRJTKDK.js +204 -0
- package/dist/chunk-AJ63GPM7.js +54 -0
- package/dist/chunk-ATJ3NXDP.js +244 -0
- package/dist/chunk-B5IS7LE4.js +128 -0
- package/dist/chunk-BFJ45HQT.js +244 -0
- package/dist/chunk-BKINEQVI.js +244 -0
- package/dist/chunk-BMHE3UQU.js +495 -0
- package/dist/chunk-BNTUZVPS.js +1921 -0
- package/dist/chunk-BOJV6NI3.js +128 -0
- package/dist/chunk-BPHWI6N2.js +284 -0
- package/dist/chunk-BXCQWWJP.js +185 -0
- package/dist/chunk-BZ6K7AY3.js +50 -0
- package/dist/chunk-C54KIFLS.js +214 -0
- package/dist/chunk-C6ODVGTC.js +818 -0
- package/dist/chunk-C6OYEJJI.js +260 -0
- package/dist/chunk-CHBGCQXG.js +333 -0
- package/dist/chunk-CHBHR5W6.js +3556 -0
- package/dist/chunk-CHUOANKE.js +346 -0
- package/dist/chunk-CSF4RUCN.js +58 -0
- package/dist/chunk-CXKHWCNN.js +204 -0
- package/dist/chunk-CZR6Z5D7.js +330 -0
- package/dist/chunk-D24ANCWY.js +204 -0
- package/dist/chunk-D2T3272U.js +171 -0
- package/dist/chunk-DBJCWK6T.js +377 -0
- package/dist/chunk-DCHEIVGT.js +221 -0
- package/dist/chunk-DF4SM6ZX.js +128 -0
- package/dist/chunk-DHIBLMSP.js +30 -0
- package/dist/chunk-DOAC6CLC.js +127 -0
- package/dist/chunk-DOGNJ4VR.js +818 -0
- package/dist/chunk-DYXJFUCI.js +818 -0
- package/dist/chunk-E2AF2WYY.js +346 -0
- package/dist/chunk-E2KZEZZW.js +1090 -0
- package/dist/chunk-E4KWB4WM.js +348 -0
- package/dist/chunk-EGR2NYID.js +50 -0
- package/dist/chunk-ENU7URWK.js +1073 -0
- package/dist/chunk-EQ5UBJGX.js +81 -0
- package/dist/chunk-EZ7KAZMC.js +132 -0
- package/dist/chunk-F4FSSHR4.js +1073 -0
- package/dist/chunk-FBRQGHSU.js +377 -0
- package/dist/chunk-FPXU56FG.js +346 -0
- package/dist/chunk-FY7HHR5I.js +128 -0
- package/dist/chunk-FZ42OCSP.js +333 -0
- package/dist/chunk-G2S2UMU4.js +159 -0
- package/dist/chunk-G33BHQCO.js +70 -0
- package/dist/chunk-G5HWDSBH.js +50 -0
- package/dist/chunk-GCBG5TFS.js +1345 -0
- package/dist/chunk-GESN6IDC.js +127 -0
- package/dist/chunk-GHD7QG6P.js +58 -0
- package/dist/chunk-GJAILPCX.js +171 -0
- package/dist/chunk-GJQTL7RX.js +127 -0
- package/dist/chunk-GKUODJS7.js +214 -0
- package/dist/chunk-GLCKDEM2.js +97 -0
- package/dist/chunk-GLDM2FOM.js +76 -0
- package/dist/chunk-GMA34SXV.js +240 -0
- package/dist/chunk-GMM2BLFB.js +127 -0
- package/dist/chunk-GNM75IOI.js +159 -0
- package/dist/chunk-GVAVEBYR.js +2091 -0
- package/dist/chunk-GYIX2HLD.js +81 -0
- package/dist/chunk-H4LLEQ3F.js +551 -0
- package/dist/chunk-HBVCBBDA.js +127 -0
- package/dist/chunk-HBYRWOH5.js +171 -0
- package/dist/chunk-HFINM2JG.js +284 -0
- package/dist/chunk-HJGHALOG.js +1345 -0
- package/dist/chunk-HOSJTLBQ.js +513 -0
- package/dist/chunk-HRB5CP43.js +13745 -0
- package/dist/chunk-IC6HVAS3.js +56 -0
- package/dist/chunk-IDCLPPIM.js +3959 -0
- package/dist/chunk-IDFJNO44.js +1051 -0
- package/dist/chunk-II5SVNBN.js +551 -0
- package/dist/chunk-IIRLKWNZ.js +50 -0
- package/dist/chunk-ILFJMEY5.js +97 -0
- package/dist/chunk-IQXLUTWC.js +50 -0
- package/dist/chunk-ISQAOSL3.js +1921 -0
- package/dist/chunk-IWXTFDLS.js +244 -0
- package/dist/chunk-J2TGVCPE.js +1090 -0
- package/dist/chunk-J4Z5GAJ4.js +551 -0
- package/dist/chunk-J6V2DCZK.js +382 -0
- package/dist/chunk-JJSDZFKM.js +1148 -0
- package/dist/chunk-JMN2KOC4.js +128 -0
- package/dist/chunk-JP4CLFLR.js +1148 -0
- package/dist/chunk-JQVYPBR2.js +81 -0
- package/dist/chunk-JTIOZHWG.js +58 -0
- package/dist/chunk-KDICWAYV.js +1345 -0
- package/dist/chunk-KMU7PFO3.js +1148 -0
- package/dist/chunk-KOBIB6WG.js +159 -0
- package/dist/chunk-KQFDDQB6.js +13696 -0
- package/dist/chunk-KZNSOHCB.js +280 -0
- package/dist/chunk-LDDCAATQ.js +1090 -0
- package/dist/chunk-LJN2O5IG.js +197 -0
- package/dist/chunk-LSIYHKDS.js +54 -0
- package/dist/chunk-LVMBYP3C.js +171 -0
- package/dist/chunk-M2WQW5NC.js +227 -0
- package/dist/chunk-MREDKOS4.js +731 -0
- package/dist/chunk-MSF2Y5MS.js +346 -0
- package/dist/chunk-MY647ZHR.js +448 -0
- package/dist/chunk-MY6SP5NZ.js +551 -0
- package/dist/chunk-MZ5CEHPQ.js +89 -0
- package/dist/chunk-N2ACW2ZG.js +363 -0
- package/dist/chunk-NESTX6DR.js +76 -0
- package/dist/chunk-NQZORF6L.js +731 -0
- package/dist/chunk-NSMJDATI.js +495 -0
- package/dist/chunk-NSQ5JE23.js +1090 -0
- package/dist/chunk-NWEFAFJS.js +197 -0
- package/dist/chunk-NYF7GHC5.js +526 -0
- package/dist/chunk-NZGGRM4P.js +731 -0
- package/dist/chunk-NZL567WG.js +81 -0
- package/dist/chunk-NZM4E6Y3.js +89 -0
- package/dist/chunk-O5OMH6LI.js +244 -0
- package/dist/chunk-O6XF6NUN.js +1090 -0
- package/dist/chunk-OBUV3W7L.js +163 -0
- package/dist/chunk-OF4KG3L7.js +1090 -0
- package/dist/chunk-OLDS7LJN.js +495 -0
- package/dist/chunk-OO2I22RX.js +38 -0
- package/dist/chunk-OPUUT33V.js +447 -0
- package/dist/chunk-OQZPSWVN.js +526 -0
- package/dist/chunk-OR6KJ5HH.js +58 -0
- package/dist/chunk-OT3VMTKB.js +50 -0
- package/dist/chunk-OV6NT6QX.js +128 -0
- package/dist/chunk-P6RVIOVA.js +157 -0
- package/dist/chunk-PDTR3YUU.js +54 -0
- package/dist/chunk-PEGTV6EJ.js +1345 -0
- package/dist/chunk-PITVTSQW.js +333 -0
- package/dist/chunk-PSUAO4MZ.js +345 -0
- package/dist/chunk-PUA5564C.js +210 -0
- package/dist/chunk-PUQLKLQX.js +731 -0
- package/dist/chunk-PWQIS5E5.js +382 -0
- package/dist/chunk-PXXHKWDH.js +818 -0
- package/dist/chunk-QOC46BDY.js +346 -0
- package/dist/chunk-QROKS65G.js +76 -0
- package/dist/chunk-R54I2N2T.js +818 -0
- package/dist/chunk-RCFYQHUP.js +818 -0
- package/dist/chunk-RJTND4YS.js +284 -0
- package/dist/chunk-RTA6KSSK.js +89 -0
- package/dist/chunk-SBLHQMMZ.js +81 -0
- package/dist/chunk-SBX6HSEO.js +159 -0
- package/dist/chunk-SEUST6U5.js +284 -0
- package/dist/chunk-SG2ANG5C.js +123 -0
- package/dist/chunk-SUEQF3ZS.js +214 -0
- package/dist/chunk-SVFNKSZV.js +333 -0
- package/dist/chunk-SWNAM2NW.js +526 -0
- package/dist/chunk-TAQT2DC7.js +330 -0
- package/dist/chunk-TB7HFW7M.js +127 -0
- package/dist/chunk-TBJP46RP.js +1148 -0
- package/dist/chunk-TS7NGPU4.js +1073 -0
- package/dist/chunk-TUPDOPMG.js +731 -0
- package/dist/chunk-TYKUZVCA.js +1921 -0
- package/dist/chunk-TZMXJVZV.js +345 -0
- package/dist/chunk-U2DCN7M6.js +1073 -0
- package/dist/chunk-UJZPLZLU.js +197 -0
- package/dist/chunk-UKRKOJQZ.js +54 -0
- package/dist/chunk-UUKDAIH2.js +731 -0
- package/dist/chunk-V6VEFEEH.js +1345 -0
- package/dist/chunk-VCVGE7HK.js +1921 -0
- package/dist/chunk-VIO2ALGH.js +290 -0
- package/dist/chunk-VK6YZ6K7.js +1073 -0
- package/dist/chunk-VKCUSNJW.js +377 -0
- package/dist/chunk-VKT4N6WM.js +495 -0
- package/dist/chunk-VQUEP7UA.js +244 -0
- package/dist/chunk-VRPPJFIQ.js +1921 -0
- package/dist/chunk-VXODHQXB.js +377 -0
- package/dist/chunk-WHK7GXFR.js +13745 -0
- package/dist/chunk-WP3PVBBP.js +204 -0
- package/dist/chunk-WQEUY7DC.js +129 -0
- package/dist/chunk-WWPJTPPQ.js +197 -0
- package/dist/chunk-WXMXUKCA.js +262 -0
- package/dist/chunk-WYZSWV6A.js +346 -0
- package/dist/chunk-X2WBH2IO.js +297 -0
- package/dist/chunk-X33TSJNO.js +394 -0
- package/dist/chunk-X7MMI2UI.js +89 -0
- package/dist/chunk-XD6VOXK3.js +159 -0
- package/dist/chunk-XG3BQZIK.js +85 -0
- package/dist/chunk-XIKBIAOS.js +75 -0
- package/dist/chunk-XPEB545Q.js +54 -0
- package/dist/chunk-XWH2MLWS.js +330 -0
- package/dist/chunk-YH7V73XW.js +89 -0
- package/dist/chunk-YHSATGMH.js +3955 -0
- package/dist/chunk-YJBCGD46.js +13745 -0
- package/dist/chunk-YLOJPYCJ.js +284 -0
- package/dist/chunk-YMLM5D65.js +135 -0
- package/dist/chunk-YNJPRQ6J.js +377 -0
- package/dist/chunk-YSNEHBI6.js +551 -0
- package/dist/chunk-Z33XSFND.js +76 -0
- package/dist/chunk-ZA7N3ZTA.js +1921 -0
- package/dist/chunk-ZD6BMW2K.js +33 -0
- package/dist/chunk-ZFRG2MNB.js +382 -0
- package/dist/chunk-ZKG5IYCG.js +668 -0
- package/dist/chunk-ZRRRSVQF.js +204 -0
- package/dist/chunk-ZU4K7ZNX.js +197 -0
- package/dist/co-activation-HZMJC34P.js +72 -0
- package/dist/co-occurrence-AVYXRV4L.js +74 -0
- package/dist/core-memory-554Q3YN5.js +110 -0
- package/dist/core-memory-BC4YN5F4.js +110 -0
- package/dist/core-memory-NID6R3YR.js +110 -0
- package/dist/core-memory-NPJCVUMF.js +110 -0
- package/dist/core-memory-OKGXL33Z.js +110 -0
- package/dist/core-memory-XHIC5NAB.js +110 -0
- package/dist/crdt-sync-ZCH55JNR.js +33 -0
- package/dist/crm-webhook-6OMVUUGR.js +10 -0
- package/dist/crm-webhook-MHZTXU5N.js +10 -0
- package/dist/crm-webhook-TMWJT2Z5.js +10 -0
- package/dist/crm-webhook-UCWF3XDB.js +10 -0
- package/dist/crm-webhook-XISULXI7.js +10 -0
- package/dist/crm-webhook-YJ5A7F2E.js +10 -0
- package/dist/cto-delegation-gate-4PMJZL2T.js +206 -0
- package/dist/cto-delegation-gate-A7YKXTRO.js +206 -0
- package/dist/cto-delegation-gate-JFZFZGC2.js +206 -0
- package/dist/cto-delegation-gate-K32M4GVM.js +206 -0
- package/dist/cto-delegation-gate-OREBAHUM.js +206 -0
- package/dist/cto-delegation-gate-POHESML5.js +206 -0
- package/dist/daemon-orchestration-2Q7BYOHC.js +135 -0
- package/dist/daemon-orchestration-4RJ2CZJL.js +135 -0
- package/dist/daemon-orchestration-HXYPHSYU.js +135 -0
- package/dist/daemon-orchestration-I5BE46P3.js +135 -0
- package/dist/daemon-orchestration-NKE4FYQS.js +135 -0
- package/dist/daemon-orchestration-YWEXRAZA.js +135 -0
- package/dist/db-backup-5GA2YFDX.js +33 -0
- package/dist/dreaming-3F72ROTL.js +32 -0
- package/dist/dreaming-I6KXO6E2.js +32 -0
- package/dist/dreaming-JD7MNJGS.js +32 -0
- package/dist/dreaming-LCKPA3B4.js +32 -0
- package/dist/dreaming-NJBK5ILR.js +32 -0
- package/dist/dreaming-SDS5IQYC.js +32 -0
- package/dist/exe-drift-VSMIMHL4.js +68 -0
- package/dist/exe-export-DVHHIA6Y.js +73 -0
- package/dist/exe-export-GIVQDENS.js +73 -0
- package/dist/exe-export-IZ2OYMT4.js +73 -0
- package/dist/exe-export-JNWX6ZCQ.js +73 -0
- package/dist/exe-export-OQXCJLWB.js +73 -0
- package/dist/exe-export-YLVAZQAV.js +73 -0
- package/dist/exe-import-7N46LSMQ.js +76 -0
- package/dist/exe-import-AEJYBLA7.js +76 -0
- package/dist/exe-import-FINYUV5T.js +76 -0
- package/dist/exe-import-HWPYARCG.js +76 -0
- package/dist/exe-import-K4TWTG24.js +76 -0
- package/dist/exe-import-LZKZQ54C.js +76 -0
- package/dist/exe-key-6FPQHBW6.js +579 -0
- package/dist/exe-key-H45JY44F.js +579 -0
- package/dist/exe-key-MAEQGTB7.js +579 -0
- package/dist/exe-key-N3XYSEXP.js +579 -0
- package/dist/exe-key-Q3ZNYT6L.js +579 -0
- package/dist/exe-key-Q47RPB45.js +579 -0
- package/dist/exe-snapshot-2USE2HHM.js +164 -0
- package/dist/exe-snapshot-3TEM3BFD.js +164 -0
- package/dist/exe-snapshot-HECGUHL3.js +164 -0
- package/dist/exe-snapshot-HZU66HXX.js +164 -0
- package/dist/exe-snapshot-L7OQWZUH.js +164 -0
- package/dist/exe-snapshot-X5N5KIVJ.js +164 -0
- package/dist/fast-db-init-3CNTADVO.js +7 -0
- package/dist/fast-db-init-C6IPNVPU.js +7 -0
- package/dist/fast-db-init-HXCS2AP5.js +7 -0
- package/dist/fast-db-init-I7CMGBAN.js +7 -0
- package/dist/fast-db-init-P6YESOUL.js +7 -0
- package/dist/fast-db-init-VDNEFVQF.js +7 -0
- package/dist/gateway/index.js +8 -8
- package/dist/git-staleness-YCEBBIVK.js +110 -0
- package/dist/git-task-sweep-C4OV2CEY.js +40 -0
- package/dist/git-task-sweep-H34STRNT.js +40 -0
- package/dist/git-task-sweep-J66SYJMW.js +40 -0
- package/dist/git-task-sweep-JYCD3ZKQ.js +40 -0
- package/dist/git-task-sweep-O723DB7F.js +40 -0
- package/dist/git-task-sweep-YL7NLDCK.js +40 -0
- package/dist/global-procedures-IHZM6C2K.js +20 -0
- package/dist/graph-auto-extract-RZQ3MHP2.js +162 -0
- package/dist/hooks/bug-report-worker.js +12 -12
- package/dist/hooks/codex-stop-task-finalizer.js +12 -12
- package/dist/hooks/commit-complete.js +12 -12
- package/dist/hooks/error-recall.js +6 -6
- package/dist/hooks/exe-heartbeat-hook.js +3 -3
- package/dist/hooks/ingest.js +6 -6
- package/dist/hooks/instructions-loaded.js +4 -4
- package/dist/hooks/manifest.json +19 -19
- package/dist/hooks/notification.js +4 -4
- package/dist/hooks/post-compact.js +11 -11
- package/dist/hooks/post-tool-combined.js +5 -5
- package/dist/hooks/pre-compact.js +12 -12
- package/dist/hooks/pre-tool-use.js +15 -15
- package/dist/hooks/prompt-submit.js +21 -21
- package/dist/hooks/session-end.js +16 -16
- package/dist/hooks/session-start.js +10 -10
- package/dist/hooks/stop.js +15 -15
- package/dist/hooks/subagent-stop.js +11 -11
- package/dist/hooks/summary-worker.js +15 -15
- package/dist/index.js +18 -18
- package/dist/installer-4EW5ZDGD.js +296 -0
- package/dist/installer-B2JTQO55.js +38 -0
- package/dist/installer-MIL352T7.js +342 -0
- package/dist/lib/agent-config.js +9 -3
- package/dist/lib/cloud-sync.js +4 -4
- package/dist/lib/consolidation.js +5 -5
- package/dist/lib/database.js +2 -2
- package/dist/lib/db.js +2 -2
- package/dist/lib/employee-templates.js +4 -4
- package/dist/lib/employees.js +2 -2
- package/dist/lib/exe-daemon.js +35 -34
- package/dist/lib/hybrid-search.js +5 -5
- package/dist/lib/identity.js +2 -2
- package/dist/lib/messaging.js +12 -10
- package/dist/lib/reminders.js +3 -3
- package/dist/lib/schedules.js +5 -5
- package/dist/lib/session-registry.js +4 -4
- package/dist/lib/skill-learning.js +4 -4
- package/dist/lib/store.js +4 -4
- package/dist/lib/task-router.js +3 -3
- package/dist/lib/tasks.js +11 -11
- package/dist/lib/tmux-routing.js +9 -9
- package/dist/lib/token-spend.js +3 -3
- package/dist/mcp/register-tools.js +54 -54
- package/dist/mcp/server.js +55 -55
- package/dist/mcp/tools/complete-reminder.js +4 -4
- package/dist/mcp/tools/create-reminder.js +4 -4
- package/dist/mcp/tools/create-task.js +13 -13
- package/dist/mcp/tools/deactivate-behavior.js +5 -5
- package/dist/mcp/tools/list-reminders.js +4 -4
- package/dist/mcp/tools/list-tasks.js +13 -13
- package/dist/mcp/tools/send-message.js +12 -12
- package/dist/mcp/tools/update-task.js +12 -12
- package/dist/mcp-http-config-OJQR246S.js +27 -0
- package/dist/memory-cards-IPULSQFA.js +174 -0
- package/dist/memory-graph-extractor-3TZZOKHY.js +17 -0
- package/dist/memory-poisoning-defense-SGUGR5YJ.js +225 -0
- package/dist/memory-reflection-H3WGCEM6.js +238 -0
- package/dist/notifications-65STXW6N.js +45 -0
- package/dist/notifications-K3JDUPL5.js +45 -0
- package/dist/notifications-KQOD66ZK.js +45 -0
- package/dist/notifications-PFK5OQEF.js +45 -0
- package/dist/notifications-VWPO6NJF.js +45 -0
- package/dist/notifications-WCSRQN2V.js +45 -0
- package/dist/orchestration-events-O5PSDEIO.js +25 -0
- package/dist/orchestrator-3D7QEVGP.js +33 -0
- package/dist/orchestrator-CC32RZO5.js +33 -0
- package/dist/orchestrator-DWAYSAFR.js +33 -0
- package/dist/orchestrator-RAPEJUOI.js +33 -0
- package/dist/orchestrator-TL37EAWA.js +33 -0
- package/dist/orchestrator-XPG6LJAI.js +33 -0
- package/dist/pipeline-router-5NT6FUC3.js +13 -0
- package/dist/pipeline-router-ADLTS6DZ.js +13 -0
- package/dist/pipeline-router-KSUXONDT.js +13 -0
- package/dist/pipeline-router-O6ZLSM6U.js +13 -0
- package/dist/pipeline-router-QKLYUYU7.js +13 -0
- package/dist/pipeline-router-W2W5XDND.js +13 -0
- package/dist/plan-limits-53NXLNDQ.js +26 -0
- package/dist/project-boot-ITN3FZMM.js +299 -0
- package/dist/projection-worker-27XX5M2W.js +964 -0
- package/dist/reranker-3KLYAHO4.js +19 -0
- package/dist/reranker-64KDRYPP.js +19 -0
- package/dist/reranker-GU7L2PJX.js +19 -0
- package/dist/reranker-MGY5A7BQ.js +19 -0
- package/dist/reranker-TZEXIJAN.js +19 -0
- package/dist/reranker-ZBX6HSU2.js +19 -0
- package/dist/review-polling-3ZZ2T26N.js +124 -0
- package/dist/review-polling-BBQUF54Q.js +124 -0
- package/dist/review-polling-FA2J2Q5O.js +124 -0
- package/dist/review-polling-MLS4BQ3N.js +124 -0
- package/dist/review-polling-RXQZPGRY.js +124 -0
- package/dist/review-polling-YBB6DKA5.js +124 -0
- package/dist/runtime/index.js +12 -12
- package/dist/session-events-EAODNMNR.js +36 -0
- package/dist/session-events-MVO6JNUL.js +36 -0
- package/dist/session-events-PRVDH3QS.js +36 -0
- package/dist/session-events-PU5OQKMB.js +36 -0
- package/dist/session-events-WWGF3B2N.js +36 -0
- package/dist/session-events-ZHXXAH6B.js +36 -0
- package/dist/session-kill-telemetry-O4TJHHOZ.js +29 -0
- package/dist/session-scope-CQXB7VMH.js +86 -0
- package/dist/session-scope-HHUMJYF6.js +86 -0
- package/dist/session-scope-M47JR2SD.js +86 -0
- package/dist/session-scope-MRQYSD5S.js +86 -0
- package/dist/session-scope-TAH5BUYW.js +86 -0
- package/dist/session-scope-UXZ6RUWC.js +86 -0
- package/dist/setup-wizard-UM2RHSBJ.js +12 -0
- package/dist/skill-refinement-447DZWNK.js +157 -0
- package/dist/skill-refinement-567JSF7L.js +157 -0
- package/dist/skill-refinement-6JVQ3TMS.js +157 -0
- package/dist/skill-refinement-MJPOHYD5.js +157 -0
- package/dist/skill-refinement-NVUBRK22.js +157 -0
- package/dist/skill-refinement-XNGD3C62.js +157 -0
- package/dist/stack-release-BAPCXMXW.js +713 -0
- package/dist/stack-release-W4TWTEZP.js +731 -0
- package/dist/steward-gate-VLE7OCKO.js +13 -0
- package/dist/task-enforcement-AZEO67N6.js +391 -0
- package/dist/task-enforcement-EOYP6IO4.js +391 -0
- package/dist/task-enforcement-FUHDL6UR.js +391 -0
- package/dist/task-enforcement-L5XQKFOV.js +391 -0
- package/dist/task-enforcement-QL3K4N3F.js +391 -0
- package/dist/task-enforcement-RJPWWEAE.js +391 -0
- package/dist/task-scope-DRQRNWB7.js +35 -0
- package/dist/task-scope-GS7TS3UV.js +35 -0
- package/dist/task-scope-KQNCP42W.js +35 -0
- package/dist/task-scope-SM5F6RD3.js +35 -0
- package/dist/task-scope-TZYMB634.js +35 -0
- package/dist/task-scope-ZVLUBS4C.js +35 -0
- package/dist/tasks-crud-4MSLJWXE.js +77 -0
- package/dist/tasks-crud-6TWWETGB.js +77 -0
- package/dist/tasks-crud-DBHYO4MM.js +77 -0
- package/dist/tasks-crud-EFYWPPEI.js +77 -0
- package/dist/tasks-crud-HIPXKRKX.js +77 -0
- package/dist/tasks-crud-JIS5B4GZ.js +77 -0
- package/dist/tasks-notify-7JBUNE7R.js +38 -0
- package/dist/tasks-notify-GEJKT5TO.js +38 -0
- package/dist/tasks-notify-OW3JDPLK.js +38 -0
- package/dist/tasks-notify-UPIJ3L4O.js +38 -0
- package/dist/tasks-notify-W5WVP2FG.js +38 -0
- package/dist/tasks-notify-YKEOYOKN.js +38 -0
- package/dist/tasks-review-5SJSFTUB.js +47 -0
- package/dist/tasks-review-DRKN34HO.js +47 -0
- package/dist/tasks-review-IQSAXXXE.js +47 -0
- package/dist/tasks-review-JHSYBR5I.js +47 -0
- package/dist/tasks-review-KWELLLS3.js +47 -0
- package/dist/tasks-review-SUJ6AKAS.js +47 -0
- package/dist/telemetry-upload-BSGOXGUP.js +739 -0
- package/dist/telemetry-upload-FPQAB6ZU.js +739 -0
- package/dist/telemetry-upload-LTX3C5HZ.js +739 -0
- package/dist/telemetry-upload-MYVBVUGE.js +739 -0
- package/dist/telemetry-upload-UPAABLGK.js +739 -0
- package/dist/telemetry-upload-UYEHBFGO.js +739 -0
- package/dist/token-budget-2CDWQU3Q.js +84 -0
- package/dist/tool-telemetry-7YS7EN7B.js +17 -0
- package/dist/tui/App.js +17 -17
- package/dist/tui-data-GDGBOS6G.js +258 -0
- package/dist/tui-data-LYUZFNO4.js +258 -0
- package/dist/tui-data-QM5BOKRF.js +258 -0
- package/dist/tui-data-VAE43SM3.js +258 -0
- package/dist/tui-data-VXF2RBVM.js +258 -0
- package/dist/tui-data-X7HT3FXF.js +258 -0
- package/dist/wiki-acl-MJIMXRQV.js +111 -0
- package/dist/worker-gate-BZBWTMCY.js +21 -0
- package/dist/worker-gate-CHVL6UGT.js +21 -0
- package/dist/worker-gate-KQFS4RJE.js +21 -0
- package/dist/worker-gate-NRP7CMS7.js +21 -0
- package/dist/worker-gate-WQGTZOSM.js +21 -0
- package/dist/worker-gate-X2YDTKTL.js +21 -0
- package/dist/workflow-engine-AKKOMJJQ.js +28 -0
- package/dist/workflow-engine-CYXRZXBM.js +28 -0
- package/dist/workflow-engine-EHWQO3LX.js +28 -0
- package/dist/workflow-engine-I3OUMSF4.js +28 -0
- package/dist/workflow-engine-KMLAXVA4.js +28 -0
- package/dist/workflow-engine-PHTLEAXP.js +28 -0
- package/dist/worktree-NLSKVRNC.js +26 -0
- package/dist/worktree-sweep-44TMEPLE.js +19 -0
- package/package.json +1 -1
- package/release-notes.json +46 -35
|
@@ -0,0 +1,3958 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureWorktree
|
|
3
|
+
} from "./chunk-KZNSOHCB.js";
|
|
4
|
+
import {
|
|
5
|
+
queueIntercom
|
|
6
|
+
} from "./chunk-5CHYEKMH.js";
|
|
7
|
+
import {
|
|
8
|
+
buildTaskResult,
|
|
9
|
+
serializeMessage
|
|
10
|
+
} from "./chunk-4JERP7NT.js";
|
|
11
|
+
import {
|
|
12
|
+
registerSession
|
|
13
|
+
} from "./chunk-OBUV3W7L.js";
|
|
14
|
+
import {
|
|
15
|
+
getTransport
|
|
16
|
+
} from "./chunk-MVW62NIZ.js";
|
|
17
|
+
import {
|
|
18
|
+
listTmuxSessions
|
|
19
|
+
} from "./chunk-CX6GL3ZJ.js";
|
|
20
|
+
import {
|
|
21
|
+
recordOrchestrationEventBestEffort
|
|
22
|
+
} from "./chunk-2NQQP3FF.js";
|
|
23
|
+
import {
|
|
24
|
+
getAgentRuntime,
|
|
25
|
+
normalizeCcModelName
|
|
26
|
+
} from "./chunk-EZ7KAZMC.js";
|
|
27
|
+
import {
|
|
28
|
+
RUNTIME_TABLE
|
|
29
|
+
} from "./chunk-NGP6LSV2.js";
|
|
30
|
+
import {
|
|
31
|
+
PlanLimitError,
|
|
32
|
+
assertEmployeeLimitSync
|
|
33
|
+
} from "./chunk-WQEUY7DC.js";
|
|
34
|
+
import {
|
|
35
|
+
getSessionKey
|
|
36
|
+
} from "./chunk-CVYC6DUW.js";
|
|
37
|
+
import {
|
|
38
|
+
getProjectName
|
|
39
|
+
} from "./chunk-OPU3NYOO.js";
|
|
40
|
+
import {
|
|
41
|
+
getAgentContext
|
|
42
|
+
} from "./chunk-GJV3WDWM.js";
|
|
43
|
+
import {
|
|
44
|
+
orgBus
|
|
45
|
+
} from "./chunk-MP2AFCGL.js";
|
|
46
|
+
import {
|
|
47
|
+
ensureAgentSymlink
|
|
48
|
+
} from "./chunk-G33BHQCO.js";
|
|
49
|
+
import {
|
|
50
|
+
expandDualPrefixTools
|
|
51
|
+
} from "./chunk-HYZV25LY.js";
|
|
52
|
+
import {
|
|
53
|
+
baseAgentName,
|
|
54
|
+
getClient,
|
|
55
|
+
getCoordinatorName,
|
|
56
|
+
getEmployee,
|
|
57
|
+
isCoordinatorName,
|
|
58
|
+
loadEmployees,
|
|
59
|
+
loadEmployeesSync
|
|
60
|
+
} from "./chunk-CHBHR5W6.js";
|
|
61
|
+
import {
|
|
62
|
+
loadDeviceId
|
|
63
|
+
} from "./chunk-MOZ2YQ54.js";
|
|
64
|
+
import {
|
|
65
|
+
EXE_AI_DIR
|
|
66
|
+
} from "./chunk-VXIMSRTO.js";
|
|
67
|
+
import {
|
|
68
|
+
atomicWriteJsonSync,
|
|
69
|
+
atomicWriteSync
|
|
70
|
+
} from "./chunk-LYH5HE24.js";
|
|
71
|
+
|
|
72
|
+
// src/lib/tmux-routing.ts
|
|
73
|
+
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
74
|
+
import { readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync5, appendFileSync as appendFileSync2, openSync, closeSync, writeSync, renameSync as renameSync2, constants } from "fs";
|
|
75
|
+
import path5 from "path";
|
|
76
|
+
import os4 from "os";
|
|
77
|
+
import { fileURLToPath } from "url";
|
|
78
|
+
|
|
79
|
+
// src/lib/cc-agent-support.ts
|
|
80
|
+
import { execSync } from "child_process";
|
|
81
|
+
var _cachedSupport = null;
|
|
82
|
+
function _resetCcAgentSupportCache() {
|
|
83
|
+
_cachedSupport = null;
|
|
84
|
+
}
|
|
85
|
+
function claudeSupportsAgentFlag() {
|
|
86
|
+
if (_cachedSupport !== null) return _cachedSupport;
|
|
87
|
+
try {
|
|
88
|
+
const helpOutput = execSync("claude --help 2>&1", {
|
|
89
|
+
encoding: "utf-8",
|
|
90
|
+
timeout: 5e3
|
|
91
|
+
});
|
|
92
|
+
_cachedSupport = /(^|\s)--agent(\b|=)/.test(helpOutput);
|
|
93
|
+
} catch {
|
|
94
|
+
_cachedSupport = false;
|
|
95
|
+
}
|
|
96
|
+
return _cachedSupport;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/lib/provider-table.ts
|
|
100
|
+
var PROVIDER_TABLE = {
|
|
101
|
+
opencode: {
|
|
102
|
+
baseUrl: "https://opencode.ai/zen/go",
|
|
103
|
+
apiKeyEnv: "OPENCODE_API_KEY",
|
|
104
|
+
defaultModel: "minimax-m2.7"
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var DEFAULT_PROVIDER = "default";
|
|
108
|
+
function detectActiveProvider(env = process.env) {
|
|
109
|
+
const baseUrl = env.ANTHROPIC_BASE_URL;
|
|
110
|
+
if (!baseUrl) return DEFAULT_PROVIDER;
|
|
111
|
+
for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
|
|
112
|
+
if (cfg.baseUrl === baseUrl) return name;
|
|
113
|
+
}
|
|
114
|
+
return DEFAULT_PROVIDER;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/lib/tmux-routing.ts
|
|
118
|
+
import { unlinkSync as unlinkSync3 } from "fs";
|
|
119
|
+
|
|
120
|
+
// src/lib/routing-log.ts
|
|
121
|
+
import { appendFileSync, mkdirSync, existsSync, statSync, renameSync } from "fs";
|
|
122
|
+
import path from "path";
|
|
123
|
+
import os from "os";
|
|
124
|
+
var LOG_DIR = path.join(os.homedir(), ".exe-os", "logs");
|
|
125
|
+
var LOG_FILE = path.join(LOG_DIR, "routing.log");
|
|
126
|
+
var dirChecked = false;
|
|
127
|
+
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
128
|
+
function ensureDir() {
|
|
129
|
+
if (dirChecked) return;
|
|
130
|
+
if (!existsSync(LOG_DIR)) mkdirSync(LOG_DIR, { recursive: true });
|
|
131
|
+
dirChecked = true;
|
|
132
|
+
}
|
|
133
|
+
function rotateIfNeeded() {
|
|
134
|
+
try {
|
|
135
|
+
const stat = statSync(LOG_FILE);
|
|
136
|
+
if (stat.size > MAX_LOG_BYTES) {
|
|
137
|
+
renameSync(LOG_FILE, LOG_FILE + ".1");
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function logRouting(event) {
|
|
143
|
+
try {
|
|
144
|
+
ensureDir();
|
|
145
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
146
|
+
const flag = event.mismatch ? " \u26A0\uFE0F MISMATCH" : "";
|
|
147
|
+
const taskRef = event.taskId ? ` task=${event.taskId.slice(0, 8)}` : "";
|
|
148
|
+
const assignee = event.assignedTo ? ` \u2192 ${event.assignedTo}` : "";
|
|
149
|
+
const details = JSON.stringify({
|
|
150
|
+
...event.inputs && { inputs: event.inputs },
|
|
151
|
+
...event.path && { path: event.path },
|
|
152
|
+
...event.result !== void 0 && { result: event.result },
|
|
153
|
+
...event.note && { note: event.note },
|
|
154
|
+
...event.taskTitle && { title: event.taskTitle }
|
|
155
|
+
});
|
|
156
|
+
const line = `[${ts}] ${event.event} | ${event.caller}${taskRef}${assignee} | result=${event.result ?? "null"}${flag} | ${details}
|
|
157
|
+
`;
|
|
158
|
+
rotateIfNeeded();
|
|
159
|
+
appendFileSync(LOG_FILE, line);
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function logSessionResolve(opts) {
|
|
164
|
+
logRouting({
|
|
165
|
+
event: "session_resolve",
|
|
166
|
+
caller: opts.caller,
|
|
167
|
+
inputs: {
|
|
168
|
+
mySession: opts.mySession,
|
|
169
|
+
alsHint: opts.alsHint || void 0,
|
|
170
|
+
envHint: opts.envHint || void 0,
|
|
171
|
+
cacheHit: opts.cacheHit,
|
|
172
|
+
liveRoots: opts.liveRoots?.join(",") || void 0
|
|
173
|
+
},
|
|
174
|
+
path: opts.path,
|
|
175
|
+
result: opts.resolved,
|
|
176
|
+
mismatch: opts.mismatch
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function logTaskCreated(opts) {
|
|
180
|
+
logRouting({
|
|
181
|
+
event: "task_created",
|
|
182
|
+
caller: "createTaskCore",
|
|
183
|
+
taskId: opts.taskId,
|
|
184
|
+
taskTitle: opts.title,
|
|
185
|
+
assignedTo: opts.assignedTo,
|
|
186
|
+
inputs: {
|
|
187
|
+
callerSession: opts.callerSession,
|
|
188
|
+
resolvedSession: opts.resolvedSession
|
|
189
|
+
},
|
|
190
|
+
result: opts.sessionScope,
|
|
191
|
+
path: `scope=${opts.sessionScope ?? "null"} file=${opts.taskFile}`,
|
|
192
|
+
mismatch: opts.mismatch
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function logTaskDispatch(opts) {
|
|
196
|
+
logRouting({
|
|
197
|
+
event: "task_dispatch",
|
|
198
|
+
caller: "create-task.mcp",
|
|
199
|
+
taskId: opts.taskId,
|
|
200
|
+
assignedTo: opts.assignedTo,
|
|
201
|
+
inputs: {
|
|
202
|
+
callerRoot: opts.callerRoot,
|
|
203
|
+
callerSource: opts.callerSource
|
|
204
|
+
},
|
|
205
|
+
result: opts.exeSession,
|
|
206
|
+
path: `dispatch=${opts.dispatchResult} session=${opts.sessionName ?? "none"}`,
|
|
207
|
+
note: opts.error,
|
|
208
|
+
mismatch: opts.callerRoot !== opts.exeSession && !!opts.callerRoot && !!opts.exeSession
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/lib/tasks-review.ts
|
|
213
|
+
import path4 from "path";
|
|
214
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
215
|
+
|
|
216
|
+
// src/lib/tasks-crud.ts
|
|
217
|
+
import crypto from "crypto";
|
|
218
|
+
import path2 from "path";
|
|
219
|
+
import os2 from "os";
|
|
220
|
+
import { execSync as execSync2 } from "child_process";
|
|
221
|
+
import { mkdir, writeFile, appendFile } from "fs/promises";
|
|
222
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
223
|
+
var TASK_COMPLETION_REPORT_HEADING = "# Task Completion Report";
|
|
224
|
+
var REPORTABLE_COMPLETION_STATUSES = /* @__PURE__ */ new Set(["needs_review"]);
|
|
225
|
+
var FILE_PATH_RE = /(?:^|\s)([\w./-]+\.(?:ts|tsx|js|jsx|json|md|yml|yaml|sql|go|py|css|scss|html|sh))(?:\b|$)/g;
|
|
226
|
+
function isStructuredTaskCompletionReport(text) {
|
|
227
|
+
return (text ?? "").trimStart().startsWith(TASK_COMPLETION_REPORT_HEADING);
|
|
228
|
+
}
|
|
229
|
+
function cleanReportLine(line) {
|
|
230
|
+
return line.replace(/^[-*]\s+/, "").replace(/^#+\s+/, "").trim();
|
|
231
|
+
}
|
|
232
|
+
function formatReportList(lines, fallback) {
|
|
233
|
+
const cleaned = lines.map(cleanReportLine).filter((line) => line.length > 0).slice(0, 12);
|
|
234
|
+
if (cleaned.length === 0) return [`- ${fallback}`];
|
|
235
|
+
return cleaned.map((line) => `- ${line}`);
|
|
236
|
+
}
|
|
237
|
+
function linesMatching(lines, pattern) {
|
|
238
|
+
return lines.filter((line) => pattern.test(line));
|
|
239
|
+
}
|
|
240
|
+
function extractMentionedArtifacts(text) {
|
|
241
|
+
const artifacts = /* @__PURE__ */ new Set();
|
|
242
|
+
for (const match of text.matchAll(FILE_PATH_RE)) {
|
|
243
|
+
if (match[1]) artifacts.add(match[1]);
|
|
244
|
+
if (artifacts.size >= 12) break;
|
|
245
|
+
}
|
|
246
|
+
return [...artifacts];
|
|
247
|
+
}
|
|
248
|
+
function agentRoleForReport(agentId) {
|
|
249
|
+
try {
|
|
250
|
+
const employee = loadEmployeesSync().find((e) => e.name === agentId);
|
|
251
|
+
return employee?.role ?? "employee";
|
|
252
|
+
} catch {
|
|
253
|
+
return "employee";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function buildTaskCompletionReport(input) {
|
|
257
|
+
const rawResult = (input.result ?? "").trim();
|
|
258
|
+
if (isStructuredTaskCompletionReport(rawResult)) return rawResult;
|
|
259
|
+
const sourceLines = rawResult.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
260
|
+
const artifacts = extractMentionedArtifacts(rawResult);
|
|
261
|
+
const verificationLines = linesMatching(
|
|
262
|
+
sourceLines,
|
|
263
|
+
/\b(test|tests|tested|verify|verified|verification|typecheck|build|vitest|npm run|curl|passed|failed)\b/i
|
|
264
|
+
);
|
|
265
|
+
const findingLines = linesMatching(
|
|
266
|
+
sourceLines,
|
|
267
|
+
/\b(finding|findings|root cause|decision|discovered|discovery|constraint|gotcha|learned)\b/i
|
|
268
|
+
);
|
|
269
|
+
const followupLines = linesMatching(
|
|
270
|
+
sourceLines,
|
|
271
|
+
/\b(follow[- ]?up|risk|risks|blocker|blocked|todo|next action|remaining|caveat)\b/i
|
|
272
|
+
);
|
|
273
|
+
const statusNote = input.requestedStatus === input.status ? input.status : `${input.status} (requested ${input.requestedStatus})`;
|
|
274
|
+
const parts = [
|
|
275
|
+
TASK_COMPLETION_REPORT_HEADING,
|
|
276
|
+
"",
|
|
277
|
+
`Task: ${input.title}`,
|
|
278
|
+
`Task ID: ${input.taskId}`,
|
|
279
|
+
`Agent: ${input.agentId} (${agentRoleForReport(input.agentId)})`,
|
|
280
|
+
`Project: ${input.projectName}`,
|
|
281
|
+
`Status: ${statusNote}`,
|
|
282
|
+
`Date: ${input.completedAt}`,
|
|
283
|
+
`Reviewer: ${input.reviewer || "not specified"}`,
|
|
284
|
+
`Task file: ${input.taskFile}`,
|
|
285
|
+
"",
|
|
286
|
+
"## Outcome",
|
|
287
|
+
...formatReportList(sourceLines, "No result summary provided."),
|
|
288
|
+
"",
|
|
289
|
+
"## What Changed",
|
|
290
|
+
...artifacts.length > 0 ? formatReportList(artifacts, "No changed files or artifacts specified.") : ["- See outcome above; no changed files or artifacts were explicitly listed."],
|
|
291
|
+
"",
|
|
292
|
+
"## Verification",
|
|
293
|
+
...formatReportList(verificationLines, "Not specified in completion result."),
|
|
294
|
+
"",
|
|
295
|
+
"## Key Findings",
|
|
296
|
+
...formatReportList(findingLines, "Not specified."),
|
|
297
|
+
"",
|
|
298
|
+
"## Follow-ups / Risks",
|
|
299
|
+
...formatReportList(followupLines, "None specified."),
|
|
300
|
+
"",
|
|
301
|
+
"## Handoff Notes",
|
|
302
|
+
"- Reviewer should verify this report against the original task requirements before closing.",
|
|
303
|
+
`- Use the task ID (${input.taskId}) and task file (${input.taskFile}) for full context.`
|
|
304
|
+
];
|
|
305
|
+
return parts.join("\n");
|
|
306
|
+
}
|
|
307
|
+
function shouldNormalizeTaskResult(requestedStatus, finalStatus) {
|
|
308
|
+
return requestedStatus === "done" || REPORTABLE_COMPLETION_STATUSES.has(finalStatus);
|
|
309
|
+
}
|
|
310
|
+
async function writeCheckpoint(input) {
|
|
311
|
+
const client = getClient();
|
|
312
|
+
const row = await resolveTask(client, input.taskId);
|
|
313
|
+
const taskId = String(row.id);
|
|
314
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
315
|
+
const blockedByIds = [];
|
|
316
|
+
if (row.blocked_by) {
|
|
317
|
+
blockedByIds.push(String(row.blocked_by));
|
|
318
|
+
}
|
|
319
|
+
const checkpoint = {
|
|
320
|
+
step: input.step,
|
|
321
|
+
context_summary: input.contextSummary,
|
|
322
|
+
files_touched: input.filesTouched ?? [],
|
|
323
|
+
blocked_by_ids: blockedByIds,
|
|
324
|
+
last_checkpoint_at: now
|
|
325
|
+
};
|
|
326
|
+
const result = await client.execute({
|
|
327
|
+
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ?,
|
|
328
|
+
last_heartbeat_at = ?, lease_expires_at = datetime(?, '+30 minutes')
|
|
329
|
+
WHERE id = ?`,
|
|
330
|
+
args: [JSON.stringify(checkpoint), now, now, now, taskId]
|
|
331
|
+
});
|
|
332
|
+
if (result.rowsAffected === 0) {
|
|
333
|
+
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
334
|
+
}
|
|
335
|
+
const countResult = await client.execute({
|
|
336
|
+
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
337
|
+
args: [taskId]
|
|
338
|
+
});
|
|
339
|
+
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
340
|
+
recordOrchestrationEventBestEffort({
|
|
341
|
+
eventType: "task.checkpoint",
|
|
342
|
+
source: "tasks-crud.writeCheckpoint",
|
|
343
|
+
taskId,
|
|
344
|
+
agentId: String(row.assigned_to ?? "unknown"),
|
|
345
|
+
sessionScope: row.session_scope ? String(row.session_scope) : null,
|
|
346
|
+
projectName: row.project_name ? String(row.project_name) : null,
|
|
347
|
+
payload: { step: input.step, checkpointCount }
|
|
348
|
+
});
|
|
349
|
+
return { checkpointCount };
|
|
350
|
+
}
|
|
351
|
+
function extractParentFromContext(contextBody) {
|
|
352
|
+
if (!contextBody) return null;
|
|
353
|
+
const match = contextBody.match(
|
|
354
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
355
|
+
);
|
|
356
|
+
return match ? match[1].toLowerCase() : null;
|
|
357
|
+
}
|
|
358
|
+
function slugify(title) {
|
|
359
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
360
|
+
}
|
|
361
|
+
var LANE_KEYWORDS = {
|
|
362
|
+
CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
|
|
363
|
+
CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
|
|
364
|
+
"Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
|
|
365
|
+
"Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
|
|
366
|
+
"Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
|
|
367
|
+
"AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
|
|
368
|
+
};
|
|
369
|
+
function buildKeywordIndex() {
|
|
370
|
+
const idx = /* @__PURE__ */ new Map();
|
|
371
|
+
for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
|
|
372
|
+
for (const kw of keywords) {
|
|
373
|
+
const existing = idx.get(kw) ?? [];
|
|
374
|
+
existing.push(role);
|
|
375
|
+
idx.set(kw, existing);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return idx;
|
|
379
|
+
}
|
|
380
|
+
var KEYWORD_INDEX = buildKeywordIndex();
|
|
381
|
+
function checkLaneAffinity(title, context, assigneeName) {
|
|
382
|
+
const employees = loadEmployeesSync();
|
|
383
|
+
const employee = employees.find((e) => e.name === assigneeName);
|
|
384
|
+
if (!employee) return void 0;
|
|
385
|
+
const assigneeRole = employee.role;
|
|
386
|
+
const text = `${title} ${context}`.toLowerCase();
|
|
387
|
+
const matchedRoles = /* @__PURE__ */ new Set();
|
|
388
|
+
for (const [keyword, roles] of KEYWORD_INDEX) {
|
|
389
|
+
if (text.includes(keyword)) {
|
|
390
|
+
for (const role of roles) matchedRoles.add(role);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (matchedRoles.size === 0) return void 0;
|
|
394
|
+
if (matchedRoles.has(assigneeRole)) return void 0;
|
|
395
|
+
if (assigneeRole === "COO") return void 0;
|
|
396
|
+
const expectedRoles = Array.from(matchedRoles).join(" or ");
|
|
397
|
+
return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
|
|
398
|
+
}
|
|
399
|
+
async function resolveTask(client, identifier, scopeSession) {
|
|
400
|
+
const scope = sessionScopeFilter(scopeSession);
|
|
401
|
+
const currentScope = typeof scope.args[0] === "string" ? String(scope.args[0]) : null;
|
|
402
|
+
const crossSessionError = (rows) => {
|
|
403
|
+
if (!currentScope || rows.length === 0) return null;
|
|
404
|
+
const unique = /* @__PURE__ */ new Map();
|
|
405
|
+
for (const row of rows) unique.set(String(row.id), row);
|
|
406
|
+
const crossSessionRows = [...unique.values()].filter((row) => {
|
|
407
|
+
const rowScope = row.session_scope === null || row.session_scope === void 0 ? null : String(row.session_scope);
|
|
408
|
+
return rowScope !== null && rowScope !== currentScope;
|
|
409
|
+
});
|
|
410
|
+
if (crossSessionRows.length === 0) return null;
|
|
411
|
+
const matches = crossSessionRows.slice(0, 5).map((row) => `${String(row.id).slice(0, 8)} "${String(row.title)}" [session:${String(row.session_scope)}]`).join(", ");
|
|
412
|
+
return new Error(
|
|
413
|
+
`Cross-session task mutation blocked: "${identifier}" belongs to another coordinator session. Current session: ${currentScope}. Matching task(s): ${matches}. Use task(action="list", cross_session=true) for read-only diagnostics, or switch to the owning session to update/close.`
|
|
414
|
+
);
|
|
415
|
+
};
|
|
416
|
+
const findUnscopedMatches = async () => {
|
|
417
|
+
const matches = [];
|
|
418
|
+
const pushRows = (rows) => {
|
|
419
|
+
for (const row of rows) {
|
|
420
|
+
if (!matches.some((m) => String(m.id) === String(row.id))) matches.push(row);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
let result2 = await client.execute({
|
|
424
|
+
sql: "SELECT * FROM tasks WHERE id = ? LIMIT 10",
|
|
425
|
+
args: [identifier]
|
|
426
|
+
});
|
|
427
|
+
pushRows(result2.rows);
|
|
428
|
+
if (/^[a-f0-9]{7,12}$/i.test(identifier)) {
|
|
429
|
+
result2 = await client.execute({
|
|
430
|
+
sql: "SELECT * FROM tasks WHERE id LIKE ? LIMIT 10",
|
|
431
|
+
args: [`${identifier}%`]
|
|
432
|
+
});
|
|
433
|
+
pushRows(result2.rows);
|
|
434
|
+
}
|
|
435
|
+
result2 = await client.execute({
|
|
436
|
+
sql: "SELECT * FROM tasks WHERE task_file LIKE ? LIMIT 10",
|
|
437
|
+
args: [`%${identifier}%`]
|
|
438
|
+
});
|
|
439
|
+
pushRows(result2.rows);
|
|
440
|
+
result2 = await client.execute({
|
|
441
|
+
sql: "SELECT * FROM tasks WHERE title LIKE ? LIMIT 10",
|
|
442
|
+
args: [`%${identifier}%`]
|
|
443
|
+
});
|
|
444
|
+
pushRows(result2.rows);
|
|
445
|
+
return matches;
|
|
446
|
+
};
|
|
447
|
+
let result = await client.execute({
|
|
448
|
+
sql: `SELECT * FROM tasks WHERE id = ?${scope.sql}`,
|
|
449
|
+
args: [identifier, ...scope.args]
|
|
450
|
+
});
|
|
451
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
452
|
+
if (/^[a-f0-9]{7,12}$/i.test(identifier)) {
|
|
453
|
+
result = await client.execute({
|
|
454
|
+
sql: `SELECT * FROM tasks WHERE id LIKE ?${scope.sql}`,
|
|
455
|
+
args: [`${identifier}%`, ...scope.args]
|
|
456
|
+
});
|
|
457
|
+
if (result.rows.length === 0) {
|
|
458
|
+
if (currentScope) {
|
|
459
|
+
const unscoped = await client.execute({
|
|
460
|
+
sql: `SELECT * FROM tasks WHERE id LIKE ?`,
|
|
461
|
+
args: [`${identifier}%`]
|
|
462
|
+
});
|
|
463
|
+
const err2 = crossSessionError(unscoped.rows);
|
|
464
|
+
if (err2) throw err2;
|
|
465
|
+
} else {
|
|
466
|
+
result = await client.execute({
|
|
467
|
+
sql: `SELECT * FROM tasks WHERE id LIKE ?`,
|
|
468
|
+
args: [`${identifier}%`]
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
473
|
+
if (result.rows.length > 1) {
|
|
474
|
+
const matches = result.rows.map((r) => `${String(r.id)} "${String(r.title)}" (${String(r.status)})`).join(", ");
|
|
475
|
+
throw new Error(
|
|
476
|
+
`Multiple tasks match short-ID "${identifier}": ${matches}. Use a longer prefix to disambiguate.`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
result = await client.execute({
|
|
481
|
+
sql: `SELECT * FROM tasks WHERE task_file LIKE ?${scope.sql}`,
|
|
482
|
+
args: [`%${identifier}%`, ...scope.args]
|
|
483
|
+
});
|
|
484
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
485
|
+
if (result.rows.length > 1) {
|
|
486
|
+
const exact = result.rows.filter(
|
|
487
|
+
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
488
|
+
);
|
|
489
|
+
if (exact.length === 1) return exact[0];
|
|
490
|
+
const candidates = exact.length > 1 ? exact : result.rows;
|
|
491
|
+
const active = candidates.filter(
|
|
492
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
493
|
+
);
|
|
494
|
+
if (active.length === 1) return active[0];
|
|
495
|
+
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
496
|
+
throw new Error(
|
|
497
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
result = await client.execute({
|
|
501
|
+
sql: `SELECT * FROM tasks WHERE title LIKE ?${scope.sql}`,
|
|
502
|
+
args: [`%${identifier}%`, ...scope.args]
|
|
503
|
+
});
|
|
504
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
505
|
+
if (result.rows.length > 1) {
|
|
506
|
+
const active = result.rows.filter(
|
|
507
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
508
|
+
);
|
|
509
|
+
if (active.length === 1) return active[0];
|
|
510
|
+
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
511
|
+
throw new Error(
|
|
512
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const unscopedMatches = await findUnscopedMatches();
|
|
516
|
+
const err = crossSessionError(unscopedMatches);
|
|
517
|
+
if (err) throw err;
|
|
518
|
+
throw new Error(`Task not found in current session: ${identifier}`);
|
|
519
|
+
}
|
|
520
|
+
function renderTaskMarkdown(f) {
|
|
521
|
+
return `# ${f.title}
|
|
522
|
+
|
|
523
|
+
## MANDATORY: When done
|
|
524
|
+
|
|
525
|
+
You MUST call update_task with status "done" and a result summary when finished.
|
|
526
|
+
If you skip this, your reviewer will not know you're done and your work won't be reviewed.
|
|
527
|
+
Do NOT let a failed commit or any error prevent you from calling update_task(done).
|
|
528
|
+
|
|
529
|
+
**ID:** ${f.id}
|
|
530
|
+
**Status:** ${f.status}
|
|
531
|
+
**Priority:** ${f.priority}
|
|
532
|
+
**Assigned by:** ${f.assignedBy}
|
|
533
|
+
**Assigned to:** ${f.assignedTo}
|
|
534
|
+
**Project:** ${f.projectName}
|
|
535
|
+
**Created:** ${f.created}${f.parentTaskId ? `
|
|
536
|
+
**Parent task:** ${f.parentTaskId}` : ""}
|
|
537
|
+
**Reviewer:** ${f.reviewer}
|
|
538
|
+
|
|
539
|
+
## Context
|
|
540
|
+
|
|
541
|
+
${f.context}
|
|
542
|
+
`;
|
|
543
|
+
}
|
|
544
|
+
async function createTaskCore(input) {
|
|
545
|
+
const client = getClient();
|
|
546
|
+
const id = crypto.randomUUID();
|
|
547
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
548
|
+
const slug = slugify(input.title);
|
|
549
|
+
let earlySessionScope = null;
|
|
550
|
+
let scopeMismatchWarning;
|
|
551
|
+
let callerSession = input.callerSession ?? "";
|
|
552
|
+
try {
|
|
553
|
+
const { resolveExeSession: resolveExeSession2 } = await import("./lib/tmux-routing.js");
|
|
554
|
+
let hasHttpAgentContext = false;
|
|
555
|
+
if (!callerSession) {
|
|
556
|
+
try {
|
|
557
|
+
const { getAgentContext: getAgentContext2 } = await import("./agent-context-AZTTMUHP.js");
|
|
558
|
+
const agentContext = getAgentContext2();
|
|
559
|
+
hasHttpAgentContext = Boolean(agentContext);
|
|
560
|
+
callerSession = agentContext?.sessionHint ?? "";
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
if (!callerSession) {
|
|
564
|
+
callerSession = process.env.EXE_SESSION_NAME || process.env.EXE_SESSION || "";
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const callerRoot = callerSession.includes("-") ? callerSession.split("-").pop() ?? "" : callerSession;
|
|
568
|
+
if (callerRoot) {
|
|
569
|
+
earlySessionScope = callerRoot;
|
|
570
|
+
} else if (hasHttpAgentContext) {
|
|
571
|
+
scopeMismatchWarning = "missing X-Exe-Session header in shared MCP daemon context; task created without session scope instead of guessing from daemon tmux focus.";
|
|
572
|
+
process.stderr.write(`[create_task] ${scopeMismatchWarning}
|
|
573
|
+
`);
|
|
574
|
+
earlySessionScope = null;
|
|
575
|
+
} else {
|
|
576
|
+
const resolved = resolveExeSession2();
|
|
577
|
+
if (resolved) {
|
|
578
|
+
process.stderr.write(
|
|
579
|
+
`[create_task] WARN: no caller session provided. Falling back to resolveExeSession()="${resolved}" (may be wrong). Fix: ensure MCP tool layer passes callerSession.
|
|
580
|
+
`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
if (resolved && input.projectName) {
|
|
584
|
+
const isCoordinatorSession = !resolved.includes("-");
|
|
585
|
+
if (isCoordinatorSession) {
|
|
586
|
+
earlySessionScope = resolved;
|
|
587
|
+
} else {
|
|
588
|
+
const { getSessionProject } = await import("./session-scope-HHUMJYF6.js");
|
|
589
|
+
const sessionProject = getSessionProject(resolved);
|
|
590
|
+
if (sessionProject && sessionProject !== input.projectName) {
|
|
591
|
+
scopeMismatchWarning = `session/project mismatch: session "${resolved}" owns "${sessionProject}" but task targets "${input.projectName}". Routed to default scope.`;
|
|
592
|
+
process.stderr.write(`[create_task] ${scopeMismatchWarning}
|
|
593
|
+
`);
|
|
594
|
+
earlySessionScope = null;
|
|
595
|
+
} else {
|
|
596
|
+
earlySessionScope = resolved;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
earlySessionScope = resolved;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
const scope = earlySessionScope ?? "default";
|
|
606
|
+
const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
|
|
607
|
+
let blockedById = null;
|
|
608
|
+
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
609
|
+
if (input.blockedBy) {
|
|
610
|
+
const blocker = await resolveTask(client, input.blockedBy, earlySessionScope);
|
|
611
|
+
blockedById = String(blocker.id);
|
|
612
|
+
}
|
|
613
|
+
let parentTaskId = null;
|
|
614
|
+
let parentRef = input.parentTaskId;
|
|
615
|
+
if (!parentRef) {
|
|
616
|
+
const extracted = extractParentFromContext(input.context);
|
|
617
|
+
if (extracted) {
|
|
618
|
+
parentRef = extracted;
|
|
619
|
+
process.stderr.write(
|
|
620
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (parentRef) {
|
|
625
|
+
try {
|
|
626
|
+
const parent = await resolveTask(client, parentRef, earlySessionScope);
|
|
627
|
+
parentTaskId = String(parent.id);
|
|
628
|
+
} catch (err) {
|
|
629
|
+
if (!input.parentTaskId) {
|
|
630
|
+
throw new Error(
|
|
631
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
throw err;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
let warning;
|
|
638
|
+
const dupScope = sessionScopeFilter(earlySessionScope);
|
|
639
|
+
const dupCheck = await client.execute({
|
|
640
|
+
sql: `SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')${dupScope.sql}`,
|
|
641
|
+
args: [input.title, input.assignedTo, ...dupScope.args]
|
|
642
|
+
});
|
|
643
|
+
if (dupCheck.rows.length > 0) {
|
|
644
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
645
|
+
}
|
|
646
|
+
if (!process.env.DISABLE_LANE_AFFINITY) {
|
|
647
|
+
const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
|
|
648
|
+
if (laneWarning) {
|
|
649
|
+
warning = warning ? `${warning}
|
|
650
|
+
${laneWarning}` : laneWarning;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (scopeMismatchWarning) {
|
|
654
|
+
warning = warning ? `${warning}
|
|
655
|
+
${scopeMismatchWarning}` : scopeMismatchWarning;
|
|
656
|
+
}
|
|
657
|
+
if (input.baseDir) {
|
|
658
|
+
try {
|
|
659
|
+
await mkdir(path2.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
660
|
+
await mkdir(path2.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
661
|
+
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
662
|
+
await ensureGitignoreExe(input.baseDir);
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const complexity = input.complexity ?? "standard";
|
|
667
|
+
const sessionScope = earlySessionScope;
|
|
668
|
+
try {
|
|
669
|
+
await client.execute({
|
|
670
|
+
sql: `INSERT OR IGNORE INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, spawn_runtime, spawn_model, device_id, founder_id, created_at, updated_at)
|
|
671
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
672
|
+
args: [
|
|
673
|
+
id,
|
|
674
|
+
input.title,
|
|
675
|
+
input.assignedTo,
|
|
676
|
+
input.assignedBy,
|
|
677
|
+
input.projectName,
|
|
678
|
+
input.priority,
|
|
679
|
+
initialStatus,
|
|
680
|
+
taskFile,
|
|
681
|
+
blockedById,
|
|
682
|
+
parentTaskId,
|
|
683
|
+
input.reviewer ?? null,
|
|
684
|
+
input.context,
|
|
685
|
+
complexity,
|
|
686
|
+
input.budgetTokens ?? null,
|
|
687
|
+
input.budgetFallbackModel ?? null,
|
|
688
|
+
0,
|
|
689
|
+
null,
|
|
690
|
+
sessionScope,
|
|
691
|
+
input.spawnRuntime ?? null,
|
|
692
|
+
input.spawnModel ?? null,
|
|
693
|
+
loadDeviceId(),
|
|
694
|
+
input.founderId ?? null,
|
|
695
|
+
now,
|
|
696
|
+
now
|
|
697
|
+
]
|
|
698
|
+
});
|
|
699
|
+
} catch (dbErr) {
|
|
700
|
+
process.stderr.write(
|
|
701
|
+
`[create-task] DB insert failed for "${input.title}": ${dbErr instanceof Error ? dbErr.message : String(dbErr)}
|
|
702
|
+
`
|
|
703
|
+
);
|
|
704
|
+
throw dbErr;
|
|
705
|
+
}
|
|
706
|
+
recordOrchestrationEventBestEffort({
|
|
707
|
+
eventType: "task.created",
|
|
708
|
+
source: "tasks-crud.createTaskCore",
|
|
709
|
+
taskId: id,
|
|
710
|
+
agentId: input.assignedTo,
|
|
711
|
+
reviewer: input.reviewer ?? null,
|
|
712
|
+
sessionScope,
|
|
713
|
+
projectName: input.projectName,
|
|
714
|
+
instanceId: null,
|
|
715
|
+
result: initialStatus,
|
|
716
|
+
payload: {
|
|
717
|
+
priority: input.priority,
|
|
718
|
+
complexity,
|
|
719
|
+
hasBlockedBy: Boolean(blockedById),
|
|
720
|
+
hasParentTask: Boolean(parentTaskId),
|
|
721
|
+
titleLength: input.title.length
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
logTaskCreated({
|
|
725
|
+
taskId: id,
|
|
726
|
+
title: input.title,
|
|
727
|
+
assignedTo: input.assignedTo,
|
|
728
|
+
sessionScope,
|
|
729
|
+
taskFile,
|
|
730
|
+
callerSession: callerSession || "(unknown)",
|
|
731
|
+
resolvedSession: earlySessionScope,
|
|
732
|
+
mismatch: !!(callerSession && earlySessionScope && (() => {
|
|
733
|
+
const cr = callerSession.includes("-") ? callerSession.split("-").pop() ?? "" : callerSession;
|
|
734
|
+
return cr && cr !== earlySessionScope;
|
|
735
|
+
})())
|
|
736
|
+
});
|
|
737
|
+
if (input.baseDir) {
|
|
738
|
+
try {
|
|
739
|
+
const EXE_OS_DIR = path2.join(os2.homedir(), ".exe-os");
|
|
740
|
+
const mdPath = path2.join(EXE_OS_DIR, taskFile);
|
|
741
|
+
const mdDir = path2.dirname(mdPath);
|
|
742
|
+
if (!existsSync2(mdDir)) await mkdir(mdDir, { recursive: true });
|
|
743
|
+
const reviewer = input.reviewer ?? input.assignedBy;
|
|
744
|
+
const mdContent = renderTaskMarkdown({
|
|
745
|
+
id,
|
|
746
|
+
title: input.title,
|
|
747
|
+
status: initialStatus,
|
|
748
|
+
priority: input.priority,
|
|
749
|
+
assignedBy: input.assignedBy,
|
|
750
|
+
assignedTo: input.assignedTo,
|
|
751
|
+
projectName: input.projectName,
|
|
752
|
+
created: now.split("T")[0],
|
|
753
|
+
reviewer,
|
|
754
|
+
context: input.context,
|
|
755
|
+
parentTaskId
|
|
756
|
+
});
|
|
757
|
+
await writeFile(mdPath, mdContent, "utf-8");
|
|
758
|
+
} catch (err) {
|
|
759
|
+
process.stderr.write(
|
|
760
|
+
`[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
|
|
761
|
+
`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
id,
|
|
767
|
+
title: input.title,
|
|
768
|
+
assignedTo: input.assignedTo,
|
|
769
|
+
assignedBy: input.assignedBy,
|
|
770
|
+
reviewer: input.reviewer ?? null,
|
|
771
|
+
projectName: input.projectName,
|
|
772
|
+
priority: input.priority,
|
|
773
|
+
status: initialStatus,
|
|
774
|
+
taskFile,
|
|
775
|
+
createdAt: now,
|
|
776
|
+
updatedAt: now,
|
|
777
|
+
warning,
|
|
778
|
+
budgetTokens: input.budgetTokens ?? null,
|
|
779
|
+
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
780
|
+
tokensUsed: 0,
|
|
781
|
+
tokensWarnedAt: null,
|
|
782
|
+
spawnRuntime: input.spawnRuntime ?? null,
|
|
783
|
+
spawnModel: input.spawnModel ?? null,
|
|
784
|
+
sessionScope
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
async function queryTaskRows(opts) {
|
|
788
|
+
const client = getClient();
|
|
789
|
+
const conditions = [];
|
|
790
|
+
const args = [];
|
|
791
|
+
if (opts.assignedTo) {
|
|
792
|
+
conditions.push("assigned_to = ?");
|
|
793
|
+
args.push(opts.assignedTo);
|
|
794
|
+
}
|
|
795
|
+
if (opts.status !== void 0) {
|
|
796
|
+
if (Array.isArray(opts.status)) {
|
|
797
|
+
conditions.push(`status IN (${opts.status.map(() => "?").join(",")})`);
|
|
798
|
+
args.push(...opts.status);
|
|
799
|
+
} else {
|
|
800
|
+
conditions.push("status = ?");
|
|
801
|
+
args.push(opts.status);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (opts.reviewer) {
|
|
805
|
+
conditions.push("reviewer = ?");
|
|
806
|
+
args.push(opts.reviewer);
|
|
807
|
+
}
|
|
808
|
+
if (opts.projectName !== null) {
|
|
809
|
+
const pn = opts.projectName !== void 0 ? opts.projectName : getProjectNameSafe();
|
|
810
|
+
if (pn) {
|
|
811
|
+
conditions.push("project_name = ?");
|
|
812
|
+
args.push(pn);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (opts.deviceId) {
|
|
816
|
+
conditions.push("(device_id = ? OR device_id IS NULL)");
|
|
817
|
+
args.push(opts.deviceId);
|
|
818
|
+
}
|
|
819
|
+
if (opts.sessionScope !== null && !opts.deviceId) {
|
|
820
|
+
if (opts.strictSession) {
|
|
821
|
+
const scope = strictSessionScopeFilter(opts.sessionScope);
|
|
822
|
+
if (scope.sql) {
|
|
823
|
+
const cleaned = scope.sql.replace(/^\s*AND\s+/, "");
|
|
824
|
+
conditions.push(cleaned);
|
|
825
|
+
args.push(...scope.args);
|
|
826
|
+
}
|
|
827
|
+
} else {
|
|
828
|
+
const scope = sessionScopeFilter(opts.sessionScope);
|
|
829
|
+
if (scope.sql) {
|
|
830
|
+
const cleaned = scope.sql.replace(/^\s*AND\s+/, "");
|
|
831
|
+
conditions.push(cleaned);
|
|
832
|
+
args.push(...scope.args);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (opts.founderId !== null) {
|
|
837
|
+
try {
|
|
838
|
+
const { founderScopeFilter } = await import("./founder-context-Q2HUCZX4.js");
|
|
839
|
+
const founderFilter = founderScopeFilter(opts.founderId);
|
|
840
|
+
const cleaned = founderFilter.sql.replace(/^\s*AND\s+/, "");
|
|
841
|
+
conditions.push(cleaned);
|
|
842
|
+
args.push(...founderFilter.args.filter((a) => a !== null));
|
|
843
|
+
} catch {
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (opts.extraConditions) {
|
|
847
|
+
conditions.push(...opts.extraConditions);
|
|
848
|
+
}
|
|
849
|
+
if (opts.extraArgs) {
|
|
850
|
+
args.push(...opts.extraArgs);
|
|
851
|
+
}
|
|
852
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
853
|
+
const cols = opts.columns ?? "*";
|
|
854
|
+
const order = opts.orderBy ?? "priority ASC, created_at DESC";
|
|
855
|
+
const limit = opts.limit ?? 100;
|
|
856
|
+
const result = await client.execute({
|
|
857
|
+
sql: `SELECT ${cols} FROM tasks ${where} ORDER BY ${order} LIMIT ?`,
|
|
858
|
+
args: [...args, limit]
|
|
859
|
+
});
|
|
860
|
+
return result.rows;
|
|
861
|
+
}
|
|
862
|
+
function getProjectNameSafe() {
|
|
863
|
+
try {
|
|
864
|
+
return getProjectName() || void 0;
|
|
865
|
+
} catch {
|
|
866
|
+
return void 0;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async function listTasks(input) {
|
|
870
|
+
const extraConditions = [];
|
|
871
|
+
const extraArgs = [];
|
|
872
|
+
if (input.priority) {
|
|
873
|
+
extraConditions.push("priority = ?");
|
|
874
|
+
extraArgs.push(input.priority);
|
|
875
|
+
}
|
|
876
|
+
const callerSession = input.callerSession?.trim();
|
|
877
|
+
if (input.isOwnQuery && callerSession) {
|
|
878
|
+
extraConditions.push("(instance_id IS NULL OR instance_id = ?)");
|
|
879
|
+
extraArgs.push(callerSession);
|
|
880
|
+
extraConditions.push(
|
|
881
|
+
"(assigned_tmux IS NULL OR assigned_tmux = 'unknown' OR assigned_tmux = ? OR assigned_tmux LIKE ?)"
|
|
882
|
+
);
|
|
883
|
+
extraArgs.push(callerSession, `${callerSession}:%`);
|
|
884
|
+
}
|
|
885
|
+
const queryOpts = {
|
|
886
|
+
assignedTo: input.assignedTo,
|
|
887
|
+
reviewer: input.reviewer,
|
|
888
|
+
status: input.status || void 0,
|
|
889
|
+
// Public listTasks() must not silently scope to process.cwd().
|
|
890
|
+
// MCP/tool callers resolve project scope explicitly before calling here.
|
|
891
|
+
// Auto-detecting from this library made isolated test DB rows invisible
|
|
892
|
+
// (fixtures use arbitrary project names) and, in the daemon, could hide
|
|
893
|
+
// real cross-project tasks because the shared daemon cwd is exe-os.
|
|
894
|
+
//
|
|
895
|
+
// Lower-level queryTaskRows() keeps its historical auto-detect behavior for
|
|
896
|
+
// daemon/hook callers that deliberately omit projectName. listTasks()
|
|
897
|
+
// defaults to project-unscoped unless the caller provides a project.
|
|
898
|
+
projectName: input.projectName === void 0 ? null : input.projectName || null,
|
|
899
|
+
// Skip session scope when querying your own tasks — agents must ALWAYS see
|
|
900
|
+
// their own work regardless of which coordinator session created the task.
|
|
901
|
+
// Bug: bob-exe1 couldn't see tasks created by exe1 because session_scope='exe1'
|
|
902
|
+
// didn't match bob-exe1's resolved scope.
|
|
903
|
+
sessionScope: input.crossSession || input.isOwnQuery ? null : void 0,
|
|
904
|
+
columns: "*",
|
|
905
|
+
limit: 1e3,
|
|
906
|
+
orderBy: input.status ? "priority ASC, created_at DESC" : "CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC",
|
|
907
|
+
// When no status filter, listTasks defaults to active + blocked
|
|
908
|
+
...input.status ? {} : { status: ["open", "in_progress", "blocked"] },
|
|
909
|
+
...extraConditions.length > 0 ? { extraConditions, extraArgs } : {}
|
|
910
|
+
};
|
|
911
|
+
let rows = await queryTaskRows(queryOpts);
|
|
912
|
+
if (rows.length === 0 && !input.crossSession && input.assignedTo) {
|
|
913
|
+
const unscopedRows = await queryTaskRows({
|
|
914
|
+
...queryOpts,
|
|
915
|
+
sessionScope: null
|
|
916
|
+
});
|
|
917
|
+
if (unscopedRows.length > 0) {
|
|
918
|
+
process.stderr.write(
|
|
919
|
+
`[list-tasks] WARN: Session-scoped query returned 0 for ${input.assignedTo} but unscoped found ${unscopedRows.length}. Possible session_scope mismatch after daemon restart. Returning unscoped results.
|
|
920
|
+
`
|
|
921
|
+
);
|
|
922
|
+
rows = unscopedRows;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return rows.map((r) => ({
|
|
926
|
+
id: String(r.id),
|
|
927
|
+
title: String(r.title),
|
|
928
|
+
assignedTo: String(r.assigned_to),
|
|
929
|
+
assignedBy: String(r.assigned_by),
|
|
930
|
+
reviewer: r.reviewer !== null && r.reviewer !== void 0 ? String(r.reviewer) : null,
|
|
931
|
+
projectName: String(r.project_name),
|
|
932
|
+
priority: String(r.priority),
|
|
933
|
+
status: String(r.status),
|
|
934
|
+
taskFile: String(r.task_file),
|
|
935
|
+
createdAt: String(r.created_at),
|
|
936
|
+
updatedAt: String(r.updated_at),
|
|
937
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
938
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
939
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
940
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
941
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null,
|
|
942
|
+
spawnRuntime: r.spawn_runtime !== null && r.spawn_runtime !== void 0 ? String(r.spawn_runtime) : null,
|
|
943
|
+
spawnModel: r.spawn_model !== null && r.spawn_model !== void 0 ? String(r.spawn_model) : null,
|
|
944
|
+
sessionScope: r.session_scope !== null && r.session_scope !== void 0 ? String(r.session_scope) : null
|
|
945
|
+
}));
|
|
946
|
+
}
|
|
947
|
+
var TMUX_ALIVE_CACHE_TTL_MS = Number(process.env.EXE_TMUX_ALIVE_CACHE_TTL_MS || 2e3);
|
|
948
|
+
var _tmuxAliveCacheTs = 0;
|
|
949
|
+
var _tmuxAliveSessions = /* @__PURE__ */ new Set();
|
|
950
|
+
var _tmuxAlivePanes = /* @__PURE__ */ new Set();
|
|
951
|
+
var _tmuxAliveCacheVerified = false;
|
|
952
|
+
function tmuxCmd(args) {
|
|
953
|
+
const sock = process.env.EXE_TMUX_SOCKET;
|
|
954
|
+
return sock ? `tmux -L ${sock} ${args}` : `tmux ${args}`;
|
|
955
|
+
}
|
|
956
|
+
function refreshTmuxAliveCacheSync() {
|
|
957
|
+
const now = Date.now();
|
|
958
|
+
if (now - _tmuxAliveCacheTs < TMUX_ALIVE_CACHE_TTL_MS) return;
|
|
959
|
+
_tmuxAliveCacheTs = now;
|
|
960
|
+
_tmuxAliveCacheVerified = false;
|
|
961
|
+
_tmuxAliveSessions = /* @__PURE__ */ new Set();
|
|
962
|
+
_tmuxAlivePanes = /* @__PURE__ */ new Set();
|
|
963
|
+
try {
|
|
964
|
+
const output = execSync2(tmuxCmd("list-panes -a -F '#{session_name} #{pane_id} #{pane_dead}'"), {
|
|
965
|
+
timeout: 2e3,
|
|
966
|
+
encoding: "utf8",
|
|
967
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
968
|
+
});
|
|
969
|
+
for (const line of output.split("\n")) {
|
|
970
|
+
const [session, paneId, paneDead] = line.split(" ");
|
|
971
|
+
if (!session) continue;
|
|
972
|
+
_tmuxAliveCacheVerified = true;
|
|
973
|
+
if (paneDead !== "1") {
|
|
974
|
+
_tmuxAliveSessions.add(session);
|
|
975
|
+
if (paneId) _tmuxAlivePanes.add(paneId);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
} catch {
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function isTmuxSessionAlive(identifier) {
|
|
982
|
+
if (!identifier || identifier === "unknown") return true;
|
|
983
|
+
refreshTmuxAliveCacheSync();
|
|
984
|
+
if (!_tmuxAliveCacheVerified) return true;
|
|
985
|
+
if (identifier.startsWith("%")) return _tmuxAlivePanes.has(identifier);
|
|
986
|
+
return _tmuxAliveSessions.has(identifier);
|
|
987
|
+
}
|
|
988
|
+
var DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
|
|
989
|
+
var TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
990
|
+
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
991
|
+
if (!taskContext) return null;
|
|
992
|
+
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
993
|
+
try {
|
|
994
|
+
const since = new Date(taskCreatedAt).toISOString();
|
|
995
|
+
const branch = execSync2(
|
|
996
|
+
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
997
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
998
|
+
).trim();
|
|
999
|
+
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
1000
|
+
const commitCount = execSync2(
|
|
1001
|
+
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
1002
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
1003
|
+
).trim();
|
|
1004
|
+
const count = parseInt(commitCount, 10);
|
|
1005
|
+
if (count === 0) {
|
|
1006
|
+
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
} catch {
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
async function updateTaskStatus(input) {
|
|
1014
|
+
const client = getClient();
|
|
1015
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1016
|
+
const row = await resolveTask(client, input.taskId);
|
|
1017
|
+
const taskId = String(row.id);
|
|
1018
|
+
const taskFile = String(row.task_file);
|
|
1019
|
+
const requestedStatus = input.status;
|
|
1020
|
+
const eventSessionScope = row.session_scope ? String(row.session_scope) : null;
|
|
1021
|
+
const eventProjectName = row.project_name ? String(row.project_name) : null;
|
|
1022
|
+
const eventAgentId = row.assigned_to ? String(row.assigned_to) : null;
|
|
1023
|
+
const previousStatus = String(row.status);
|
|
1024
|
+
if (input.status === "done") {
|
|
1025
|
+
input.status = "needs_review";
|
|
1026
|
+
}
|
|
1027
|
+
const isReviewTransition = input.status === "needs_review" && previousStatus !== "needs_review" && previousStatus !== "closed";
|
|
1028
|
+
if (isReviewTransition && row.reviewer) {
|
|
1029
|
+
const reviewer = String(row.reviewer);
|
|
1030
|
+
try {
|
|
1031
|
+
const { sendIntercom: sendIntercom2, employeeSessionName: employeeSessionName2, resolveExeSession: resolveExeSession2, isExeSession: isExeSession2 } = await import("./lib/tmux-routing.js");
|
|
1032
|
+
const exeSession = row.session_scope ? String(row.session_scope) : resolveExeSession2();
|
|
1033
|
+
if (exeSession) {
|
|
1034
|
+
if (isCoordinatorName(reviewer)) {
|
|
1035
|
+
if (isExeSession2(exeSession)) {
|
|
1036
|
+
sendIntercom2(exeSession, { force: true, reason: "completion" });
|
|
1037
|
+
process.stderr.write(
|
|
1038
|
+
`[tasks-crud] Review intercom sent to ${exeSession}: "${String(row.title)}" by ${String(row.assigned_to)}
|
|
1039
|
+
`
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
} else {
|
|
1043
|
+
const reviewerSession = employeeSessionName2(reviewer, exeSession);
|
|
1044
|
+
sendIntercom2(reviewerSession, { force: true, reason: "completion" });
|
|
1045
|
+
process.stderr.write(
|
|
1046
|
+
`[tasks-crud] Review intercom sent to ${reviewerSession}: "${String(row.title)}" by ${String(row.assigned_to)}
|
|
1047
|
+
`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
process.stderr.write(
|
|
1053
|
+
`[tasks-crud] Review intercom failed for ${reviewer}: ${err instanceof Error ? err.message : String(err)}
|
|
1054
|
+
`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
const { reviewSignalPath, taskSignalDir } = await import("./signal-paths-4GOIRLGH.js");
|
|
1059
|
+
const scope = row.session_scope ? String(row.session_scope) : "default";
|
|
1060
|
+
const dir = taskSignalDir(scope);
|
|
1061
|
+
const { mkdirSync: mkdirSync3, writeFileSync: writeFileSync2 } = await import("fs");
|
|
1062
|
+
mkdirSync3(dir, { recursive: true });
|
|
1063
|
+
writeFileSync2(
|
|
1064
|
+
reviewSignalPath(scope, reviewer, String(row.id)),
|
|
1065
|
+
JSON.stringify({
|
|
1066
|
+
task_id: String(row.id),
|
|
1067
|
+
title: String(row.title),
|
|
1068
|
+
assigned_to: String(row.assigned_to),
|
|
1069
|
+
reviewer,
|
|
1070
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1071
|
+
}),
|
|
1072
|
+
"utf-8"
|
|
1073
|
+
);
|
|
1074
|
+
} catch {
|
|
1075
|
+
}
|
|
1076
|
+
try {
|
|
1077
|
+
const { writeNotification: writeNotification2 } = await import("./notifications-VWPO6NJF.js");
|
|
1078
|
+
await writeNotification2({
|
|
1079
|
+
agentId: reviewer,
|
|
1080
|
+
agentRole: isCoordinatorName(reviewer) ? "COO" : "manager",
|
|
1081
|
+
event: "task_complete",
|
|
1082
|
+
project: String(row.project_name ?? ""),
|
|
1083
|
+
summary: `${String(row.assigned_to)} completed "${String(row.title)}" \u2014 needs your review.`,
|
|
1084
|
+
taskFile,
|
|
1085
|
+
sessionScope: row.session_scope ? String(row.session_scope) : void 0
|
|
1086
|
+
});
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const isBlockedTransition = input.status === "blocked" && previousStatus !== "blocked";
|
|
1091
|
+
if (isBlockedTransition && row.assigned_by) {
|
|
1092
|
+
const dispatcher = String(row.assigned_by);
|
|
1093
|
+
try {
|
|
1094
|
+
const { sendIntercom: sendIntercom2, employeeSessionName: employeeSessionName2, resolveExeSession: resolveExeSession2, isExeSession: isExeSession2 } = await import("./lib/tmux-routing.js");
|
|
1095
|
+
const exeSession = row.session_scope ? String(row.session_scope) : resolveExeSession2();
|
|
1096
|
+
if (exeSession) {
|
|
1097
|
+
if (isCoordinatorName(dispatcher)) {
|
|
1098
|
+
if (isExeSession2(exeSession)) {
|
|
1099
|
+
sendIntercom2(exeSession, { force: true, reason: "completion" });
|
|
1100
|
+
process.stderr.write(
|
|
1101
|
+
`[tasks-crud] Blocked intercom sent to ${exeSession}: "${String(row.title)}" blocked by ${String(row.assigned_to)}
|
|
1102
|
+
`
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
} else {
|
|
1106
|
+
const dispatcherSession = employeeSessionName2(dispatcher, exeSession);
|
|
1107
|
+
sendIntercom2(dispatcherSession, { force: true, reason: "completion" });
|
|
1108
|
+
process.stderr.write(
|
|
1109
|
+
`[tasks-crud] Blocked intercom sent to ${dispatcherSession}: "${String(row.title)}" blocked by ${String(row.assigned_to)}
|
|
1110
|
+
`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
process.stderr.write(
|
|
1116
|
+
`[tasks-crud] Blocked intercom failed for ${dispatcher}: ${err instanceof Error ? err.message : String(err)}
|
|
1117
|
+
`
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const VALID_TRANSITIONS = {
|
|
1122
|
+
open: ["in_progress", "needs_review", "cancelled", "blocked"],
|
|
1123
|
+
in_progress: ["in_progress", "open", "needs_review", "blocked", "cancelled"],
|
|
1124
|
+
blocked: ["open", "in_progress", "cancelled"],
|
|
1125
|
+
needs_review: ["closed", "in_progress", "open"],
|
|
1126
|
+
closed: [],
|
|
1127
|
+
// terminal — no transitions out
|
|
1128
|
+
cancelled: ["open"]
|
|
1129
|
+
// allow re-opening cancelled tasks
|
|
1130
|
+
};
|
|
1131
|
+
const currentStatus = String(row.status);
|
|
1132
|
+
const validNext = VALID_TRANSITIONS[currentStatus];
|
|
1133
|
+
if (validNext && !validNext.includes(input.status)) {
|
|
1134
|
+
throw new Error(
|
|
1135
|
+
`Invalid task transition: ${currentStatus} \u2192 ${input.status}. Valid transitions from "${currentStatus}": [${validNext.join(", ")}]`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
1139
|
+
process.stderr.write(
|
|
1140
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
1141
|
+
`
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
if (requestedStatus === "done") {
|
|
1145
|
+
const existingRow = await client.execute({
|
|
1146
|
+
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
1147
|
+
args: [taskId]
|
|
1148
|
+
});
|
|
1149
|
+
if (existingRow.rows.length > 0) {
|
|
1150
|
+
const ctx = existingRow.rows[0];
|
|
1151
|
+
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
1152
|
+
if (warning) {
|
|
1153
|
+
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
1154
|
+
|
|
1155
|
+
${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
1156
|
+
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
1157
|
+
`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const hasStructuredFields = input.filesChanged || input.decisions || input.commits || input.testsStatus || input.buildStatus || input.prNumber || input.openQuestions;
|
|
1162
|
+
if (hasStructuredFields && (requestedStatus === "done" || input.status === "needs_review")) {
|
|
1163
|
+
const agentId = String(row.assigned_to ?? "unknown");
|
|
1164
|
+
const typedResult = buildTaskResult({
|
|
1165
|
+
from: agentId,
|
|
1166
|
+
task_id: taskId,
|
|
1167
|
+
summary: input.result ?? "",
|
|
1168
|
+
files_changed: input.filesChanged,
|
|
1169
|
+
decisions: input.decisions,
|
|
1170
|
+
commits: input.commits,
|
|
1171
|
+
tests: input.testsStatus,
|
|
1172
|
+
build: input.buildStatus,
|
|
1173
|
+
pr_number: input.prNumber,
|
|
1174
|
+
open_questions: input.openQuestions
|
|
1175
|
+
});
|
|
1176
|
+
const serialized = serializeMessage(typedResult);
|
|
1177
|
+
try {
|
|
1178
|
+
await client.execute({
|
|
1179
|
+
sql: "UPDATE tasks SET structured_result = ?, updated_at = ? WHERE id = ?",
|
|
1180
|
+
args: [serialized, now, taskId]
|
|
1181
|
+
});
|
|
1182
|
+
} catch {
|
|
1183
|
+
process.stderr.write(
|
|
1184
|
+
`[tasks-crud] structured_result column unavailable \u2014 embedding in result text
|
|
1185
|
+
`
|
|
1186
|
+
);
|
|
1187
|
+
input.result = `<!-- STRUCTURED_RESULT:${serialized} -->
|
|
1188
|
+
${input.result ?? ""}`;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const shouldWriteCompletionMemory = shouldNormalizeTaskResult(requestedStatus, input.status) && previousStatus !== input.status && (requestedStatus === "done" || input.status === "needs_review");
|
|
1192
|
+
if (shouldNormalizeTaskResult(requestedStatus, input.status)) {
|
|
1193
|
+
input.result = buildTaskCompletionReport({
|
|
1194
|
+
taskId,
|
|
1195
|
+
title: String(row.title),
|
|
1196
|
+
agentId: String(row.assigned_to),
|
|
1197
|
+
projectName: String(row.project_name ?? ""),
|
|
1198
|
+
status: input.status,
|
|
1199
|
+
requestedStatus,
|
|
1200
|
+
taskFile,
|
|
1201
|
+
reviewer: row.reviewer ? String(row.reviewer) : null,
|
|
1202
|
+
completedAt: now,
|
|
1203
|
+
result: input.result
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
if (input.status === "in_progress") {
|
|
1207
|
+
const tmuxSession = input.callerSession?.trim() || process.env.TMUX_PANE || process.env.MY_TMUX_SESSION || "unknown";
|
|
1208
|
+
const hasConcreteSessionName = tmuxSession !== "unknown" && !tmuxSession.startsWith("%");
|
|
1209
|
+
const claimGuardSql = hasConcreteSessionName ? ` AND (instance_id IS NULL OR instance_id = ?)
|
|
1210
|
+
AND (assigned_tmux IS NULL OR assigned_tmux = 'unknown' OR assigned_tmux = ? OR assigned_tmux LIKE ?)` : "";
|
|
1211
|
+
const claimGuardArgs = hasConcreteSessionName ? [tmuxSession, tmuxSession, `${tmuxSession}:%`] : [];
|
|
1212
|
+
const claimAgent = input.callerAgentId ?? process.env.AGENT_ID ?? String(row.assigned_to ?? "unknown");
|
|
1213
|
+
const leaseMinutes = 30;
|
|
1214
|
+
const estMin = input.estimatedMinutes != null ? Math.max(1, Math.round(input.estimatedMinutes)) : null;
|
|
1215
|
+
const estConf = input.estimateConfidence != null ? Math.max(0, Math.min(1, input.estimateConfidence)) : null;
|
|
1216
|
+
const claim = await client.execute({
|
|
1217
|
+
sql: `UPDATE tasks
|
|
1218
|
+
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?,
|
|
1219
|
+
claimed_by = ?, claimed_at = ?, last_heartbeat_at = ?,
|
|
1220
|
+
lease_expires_at = datetime(?, '+${leaseMinutes} minutes'),
|
|
1221
|
+
claim_count = COALESCE(claim_count, 0) + 1,
|
|
1222
|
+
started_at = COALESCE(started_at, ?),
|
|
1223
|
+
estimated_minutes = COALESCE(?, estimated_minutes),
|
|
1224
|
+
estimate_confidence = COALESCE(?, estimate_confidence)
|
|
1225
|
+
WHERE id = ? AND status = 'open'${claimGuardSql}`,
|
|
1226
|
+
args: [tmuxSession, now, claimAgent, now, now, now, now, estMin, estConf, taskId, ...claimGuardArgs]
|
|
1227
|
+
});
|
|
1228
|
+
if (claim.rowsAffected === 0) {
|
|
1229
|
+
const current = await client.execute({
|
|
1230
|
+
sql: "SELECT status, assigned_tmux, assigned_by FROM tasks WHERE id = ?",
|
|
1231
|
+
args: [taskId]
|
|
1232
|
+
});
|
|
1233
|
+
const cur = current.rows[0];
|
|
1234
|
+
const curStatus = cur?.status ?? "unknown";
|
|
1235
|
+
const claimedBySession = cur?.assigned_tmux ?? "";
|
|
1236
|
+
const assignedBy = cur?.assigned_by ?? "";
|
|
1237
|
+
if (curStatus === "in_progress" && claimedBySession && !isTmuxSessionAlive(claimedBySession)) {
|
|
1238
|
+
process.stderr.write(
|
|
1239
|
+
`[tasks] Auto-releasing dead claim on ${taskId} (was ${claimedBySession})
|
|
1240
|
+
`
|
|
1241
|
+
);
|
|
1242
|
+
await client.execute({
|
|
1243
|
+
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 = ?",
|
|
1244
|
+
args: [now, taskId]
|
|
1245
|
+
});
|
|
1246
|
+
const retried = await client.execute({
|
|
1247
|
+
sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ?,
|
|
1248
|
+
claimed_by = ?, claimed_at = ?, last_heartbeat_at = ?,
|
|
1249
|
+
lease_expires_at = datetime(?, '+${leaseMinutes} minutes'),
|
|
1250
|
+
claim_count = COALESCE(claim_count, 0) + 1
|
|
1251
|
+
WHERE id = ? AND status = 'open'${claimGuardSql}`,
|
|
1252
|
+
args: [tmuxSession, now, claimAgent, now, now, now, taskId, ...claimGuardArgs]
|
|
1253
|
+
});
|
|
1254
|
+
if (retried.rowsAffected > 0) {
|
|
1255
|
+
try {
|
|
1256
|
+
await writeCheckpoint({
|
|
1257
|
+
taskId,
|
|
1258
|
+
step: "reclaimed_dead_session",
|
|
1259
|
+
contextSummary: `Task reclaimed after dead session ${claimedBySession} released.`
|
|
1260
|
+
});
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
recordOrchestrationEventBestEffort({
|
|
1264
|
+
eventType: "task.claimed",
|
|
1265
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1266
|
+
taskId,
|
|
1267
|
+
agentId: eventAgentId,
|
|
1268
|
+
sessionScope: eventSessionScope,
|
|
1269
|
+
projectName: eventProjectName,
|
|
1270
|
+
tmuxSession,
|
|
1271
|
+
result: "reclaimed_dead_session"
|
|
1272
|
+
});
|
|
1273
|
+
recordOrchestrationEventBestEffort({
|
|
1274
|
+
eventType: "task.in_progress",
|
|
1275
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1276
|
+
taskId,
|
|
1277
|
+
agentId: eventAgentId,
|
|
1278
|
+
sessionScope: eventSessionScope,
|
|
1279
|
+
projectName: eventProjectName,
|
|
1280
|
+
tmuxSession,
|
|
1281
|
+
result: "reclaimed_dead_session"
|
|
1282
|
+
});
|
|
1283
|
+
return { row, taskFile, now, taskId };
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
|
|
1287
|
+
process.stderr.write(
|
|
1288
|
+
`[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
|
|
1289
|
+
`
|
|
1290
|
+
);
|
|
1291
|
+
await client.execute({
|
|
1292
|
+
sql: `UPDATE tasks SET status = 'in_progress', assigned_tmux = ?, updated_at = ?,
|
|
1293
|
+
claimed_by = ?, claimed_at = ?, last_heartbeat_at = ?,
|
|
1294
|
+
lease_expires_at = datetime(?, '+${leaseMinutes} minutes'),
|
|
1295
|
+
claim_count = COALESCE(claim_count, 0) + 1
|
|
1296
|
+
WHERE id = ?`,
|
|
1297
|
+
args: [tmuxSession, now, claimAgent, now, now, now, taskId]
|
|
1298
|
+
});
|
|
1299
|
+
try {
|
|
1300
|
+
await writeCheckpoint({
|
|
1301
|
+
taskId,
|
|
1302
|
+
step: "assigner_override",
|
|
1303
|
+
contextSummary: `Task force-reclaimed by assigner ${input.callerAgentId}.`
|
|
1304
|
+
});
|
|
1305
|
+
} catch {
|
|
1306
|
+
}
|
|
1307
|
+
recordOrchestrationEventBestEffort({
|
|
1308
|
+
eventType: "task.claimed",
|
|
1309
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1310
|
+
taskId,
|
|
1311
|
+
agentId: eventAgentId,
|
|
1312
|
+
sessionScope: eventSessionScope,
|
|
1313
|
+
projectName: eventProjectName,
|
|
1314
|
+
tmuxSession,
|
|
1315
|
+
result: "assigner_override",
|
|
1316
|
+
payload: { callerAgentId: input.callerAgentId }
|
|
1317
|
+
});
|
|
1318
|
+
recordOrchestrationEventBestEffort({
|
|
1319
|
+
eventType: "task.in_progress",
|
|
1320
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1321
|
+
taskId,
|
|
1322
|
+
agentId: eventAgentId,
|
|
1323
|
+
sessionScope: eventSessionScope,
|
|
1324
|
+
projectName: eventProjectName,
|
|
1325
|
+
tmuxSession,
|
|
1326
|
+
result: "assigner_override"
|
|
1327
|
+
});
|
|
1328
|
+
return { row, taskFile, now, taskId };
|
|
1329
|
+
}
|
|
1330
|
+
const claimedBy = claimedBySession ? ` (claimed by ${claimedBySession})` : "";
|
|
1331
|
+
recordOrchestrationEventBestEffort({
|
|
1332
|
+
eventType: "claim.collision",
|
|
1333
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1334
|
+
severity: "warn",
|
|
1335
|
+
taskId,
|
|
1336
|
+
agentId: eventAgentId,
|
|
1337
|
+
sessionScope: eventSessionScope,
|
|
1338
|
+
projectName: eventProjectName,
|
|
1339
|
+
tmuxSession,
|
|
1340
|
+
result: curStatus,
|
|
1341
|
+
payload: { hasClaimedBy: Boolean(claimedBySession) }
|
|
1342
|
+
});
|
|
1343
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${curStatus}${claimedBy}`);
|
|
1344
|
+
}
|
|
1345
|
+
try {
|
|
1346
|
+
await writeCheckpoint({
|
|
1347
|
+
taskId,
|
|
1348
|
+
step: "claimed",
|
|
1349
|
+
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
1350
|
+
});
|
|
1351
|
+
} catch {
|
|
1352
|
+
}
|
|
1353
|
+
recordOrchestrationEventBestEffort({
|
|
1354
|
+
eventType: "task.claimed",
|
|
1355
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1356
|
+
taskId,
|
|
1357
|
+
agentId: eventAgentId,
|
|
1358
|
+
sessionScope: eventSessionScope,
|
|
1359
|
+
projectName: eventProjectName,
|
|
1360
|
+
tmuxSession,
|
|
1361
|
+
result: "open_to_in_progress"
|
|
1362
|
+
});
|
|
1363
|
+
recordOrchestrationEventBestEffort({
|
|
1364
|
+
eventType: "task.in_progress",
|
|
1365
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1366
|
+
taskId,
|
|
1367
|
+
agentId: eventAgentId,
|
|
1368
|
+
sessionScope: eventSessionScope,
|
|
1369
|
+
projectName: eventProjectName,
|
|
1370
|
+
tmuxSession,
|
|
1371
|
+
result: "open_to_in_progress"
|
|
1372
|
+
});
|
|
1373
|
+
if (input.assertions) {
|
|
1374
|
+
try {
|
|
1375
|
+
const parsed = typeof input.assertions === "string" ? JSON.parse(input.assertions) : input.assertions;
|
|
1376
|
+
const assertionsArr = Array.isArray(parsed) ? parsed : [];
|
|
1377
|
+
const VALID_ASSERTION_TYPES = ["diagnosis", "scope", "estimate", "approach", "build", "judgment"];
|
|
1378
|
+
for (const a of assertionsArr) {
|
|
1379
|
+
if (!a.type || !VALID_ASSERTION_TYPES.includes(a.type)) {
|
|
1380
|
+
throw new Error(`Invalid assertion type: ${a.type}. Valid: ${VALID_ASSERTION_TYPES.join(", ")}`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
const payload = JSON.stringify({ assertions: assertionsArr });
|
|
1384
|
+
await client.execute({
|
|
1385
|
+
sql: "UPDATE tasks SET assertions = ?, updated_at = ? WHERE id = ?",
|
|
1386
|
+
args: [payload, now, taskId]
|
|
1387
|
+
});
|
|
1388
|
+
} catch (e) {
|
|
1389
|
+
process.stderr.write(
|
|
1390
|
+
`[tasks-crud] assertions parse/store failed: ${e instanceof Error ? e.message : String(e)}
|
|
1391
|
+
`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return { row, taskFile, now, taskId };
|
|
1396
|
+
}
|
|
1397
|
+
if (input.assertionsResolved && (input.status === "needs_review" || input.status === "closed")) {
|
|
1398
|
+
try {
|
|
1399
|
+
const resolvedArr = typeof input.assertionsResolved === "string" ? JSON.parse(input.assertionsResolved) : input.assertionsResolved;
|
|
1400
|
+
if (!Array.isArray(resolvedArr)) throw new Error("assertions_resolved must be an array");
|
|
1401
|
+
const VALID_WHY_WRONG = [
|
|
1402
|
+
"missing_information",
|
|
1403
|
+
"wrong_diagnosis",
|
|
1404
|
+
"wrong_approach",
|
|
1405
|
+
"scope_underestimate",
|
|
1406
|
+
"scope_overestimate",
|
|
1407
|
+
"external_dependency",
|
|
1408
|
+
"changed_requirements"
|
|
1409
|
+
];
|
|
1410
|
+
for (const r of resolvedArr) {
|
|
1411
|
+
if (r.why_wrong && !VALID_WHY_WRONG.includes(r.why_wrong)) {
|
|
1412
|
+
throw new Error(`Invalid why_wrong: ${r.why_wrong}. Valid: ${VALID_WHY_WRONG.join(", ")}`);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
const existing = await client.execute({
|
|
1416
|
+
sql: "SELECT assertions FROM tasks WHERE id = ?",
|
|
1417
|
+
args: [taskId]
|
|
1418
|
+
});
|
|
1419
|
+
const merged = { assertions: [], resolved: [] };
|
|
1420
|
+
if (existing.rows.length > 0 && existing.rows[0]?.assertions) {
|
|
1421
|
+
try {
|
|
1422
|
+
const prev = JSON.parse(String(existing.rows[0].assertions));
|
|
1423
|
+
merged.assertions = prev.assertions ?? [];
|
|
1424
|
+
} catch {
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
merged.resolved = resolvedArr;
|
|
1428
|
+
await client.execute({
|
|
1429
|
+
sql: "UPDATE tasks SET assertions = ?, updated_at = ? WHERE id = ?",
|
|
1430
|
+
args: [JSON.stringify(merged), now, taskId]
|
|
1431
|
+
});
|
|
1432
|
+
} catch (e) {
|
|
1433
|
+
process.stderr.write(
|
|
1434
|
+
`[tasks-crud] assertions_resolved parse/store failed: ${e instanceof Error ? e.message : String(e)}
|
|
1435
|
+
`
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
let actualMinutesClause = "";
|
|
1440
|
+
let actualMinutesArgs = [];
|
|
1441
|
+
if (input.status === "needs_review" || requestedStatus === "done") {
|
|
1442
|
+
const startedAt = row.started_at ? String(row.started_at) : null;
|
|
1443
|
+
if (startedAt) {
|
|
1444
|
+
const startMs = new Date(startedAt).getTime();
|
|
1445
|
+
const nowMs = new Date(now).getTime();
|
|
1446
|
+
if (!Number.isNaN(startMs) && nowMs > startMs) {
|
|
1447
|
+
const actualMin = Math.round((nowMs - startMs) / 6e4);
|
|
1448
|
+
actualMinutesClause = ", actual_minutes = ?";
|
|
1449
|
+
actualMinutesArgs = [actualMin];
|
|
1450
|
+
const estMin = row.estimated_minutes ? Number(row.estimated_minutes) : null;
|
|
1451
|
+
if (estMin && estMin > 0) {
|
|
1452
|
+
const ratio = actualMin / estMin;
|
|
1453
|
+
const accuracy = ratio <= 1 ? `${Math.round(ratio * 100)}% of estimate` : `${ratio.toFixed(1)}x over estimate`;
|
|
1454
|
+
process.stderr.write(
|
|
1455
|
+
`[tasks] Time estimation: estimated=${estMin}min actual=${actualMin}min (${accuracy}) \u2014 ${String(row.assigned_to)}
|
|
1456
|
+
`
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (input.result) {
|
|
1463
|
+
await client.execute({
|
|
1464
|
+
sql: `UPDATE tasks SET status = ?, result = ?, updated_at = ?${actualMinutesClause} WHERE id = ?`,
|
|
1465
|
+
args: [input.status, input.result, now, ...actualMinutesArgs, taskId]
|
|
1466
|
+
});
|
|
1467
|
+
} else {
|
|
1468
|
+
await client.execute({
|
|
1469
|
+
sql: `UPDATE tasks SET status = ?, updated_at = ?${actualMinutesClause} WHERE id = ?`,
|
|
1470
|
+
args: [input.status, now, ...actualMinutesArgs, taskId]
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
if (input.status === "cancelled" || input.status === "closed") {
|
|
1474
|
+
try {
|
|
1475
|
+
await client.execute({
|
|
1476
|
+
sql: `UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0`,
|
|
1477
|
+
args: [taskFile]
|
|
1478
|
+
});
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (requestedStatus === "done") {
|
|
1483
|
+
recordOrchestrationEventBestEffort({
|
|
1484
|
+
eventType: "task.done",
|
|
1485
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1486
|
+
taskId,
|
|
1487
|
+
agentId: eventAgentId,
|
|
1488
|
+
reviewer: row.reviewer ? String(row.reviewer) : null,
|
|
1489
|
+
sessionScope: eventSessionScope,
|
|
1490
|
+
projectName: eventProjectName,
|
|
1491
|
+
result: input.status
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if (shouldWriteCompletionMemory && input.result) {
|
|
1495
|
+
try {
|
|
1496
|
+
const { writeMemoryViaDaemon } = await import("./memory-queue-client-O57KWE6B.js");
|
|
1497
|
+
await writeMemoryViaDaemon({
|
|
1498
|
+
raw_text: input.result,
|
|
1499
|
+
agent_id: String(row.assigned_to),
|
|
1500
|
+
agent_role: agentRoleForReport(String(row.assigned_to)),
|
|
1501
|
+
session_id: eventSessionScope ?? "task-completion",
|
|
1502
|
+
tool_name: "task-completion-report",
|
|
1503
|
+
project_name: eventProjectName ?? "",
|
|
1504
|
+
timestamp: now,
|
|
1505
|
+
importance: 8,
|
|
1506
|
+
task_id: taskId,
|
|
1507
|
+
memory_type: "checkpoint",
|
|
1508
|
+
trajectory: "completed",
|
|
1509
|
+
session_scope: eventSessionScope ?? void 0
|
|
1510
|
+
});
|
|
1511
|
+
} catch {
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (input.status === "needs_review" && previousStatus !== "needs_review" && previousStatus !== "closed") {
|
|
1515
|
+
recordOrchestrationEventBestEffort({
|
|
1516
|
+
eventType: "review.ready",
|
|
1517
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1518
|
+
taskId,
|
|
1519
|
+
agentId: eventAgentId,
|
|
1520
|
+
reviewer: row.reviewer ? String(row.reviewer) : null,
|
|
1521
|
+
sessionScope: eventSessionScope,
|
|
1522
|
+
projectName: eventProjectName,
|
|
1523
|
+
result: "needs_review"
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
if (input.status === "closed") {
|
|
1527
|
+
recordOrchestrationEventBestEffort({
|
|
1528
|
+
eventType: "task.closed",
|
|
1529
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1530
|
+
taskId,
|
|
1531
|
+
agentId: eventAgentId,
|
|
1532
|
+
reviewer: row.reviewer ? String(row.reviewer) : null,
|
|
1533
|
+
sessionScope: eventSessionScope,
|
|
1534
|
+
projectName: eventProjectName,
|
|
1535
|
+
result: "closed"
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
if (input.status === "blocked" && previousStatus !== "blocked") {
|
|
1539
|
+
recordOrchestrationEventBestEffort({
|
|
1540
|
+
eventType: "task.blocked",
|
|
1541
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1542
|
+
taskId,
|
|
1543
|
+
agentId: eventAgentId,
|
|
1544
|
+
sessionScope: eventSessionScope,
|
|
1545
|
+
projectName: eventProjectName,
|
|
1546
|
+
payload: { blockedBy: row.blocked_by ? String(row.blocked_by) : null }
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
if (previousStatus === "blocked" && input.status !== "blocked") {
|
|
1550
|
+
recordOrchestrationEventBestEffort({
|
|
1551
|
+
eventType: "task.unblocked",
|
|
1552
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1553
|
+
taskId,
|
|
1554
|
+
agentId: eventAgentId,
|
|
1555
|
+
sessionScope: eventSessionScope,
|
|
1556
|
+
projectName: eventProjectName,
|
|
1557
|
+
result: input.status
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
if (input.status === "cancelled") {
|
|
1561
|
+
recordOrchestrationEventBestEffort({
|
|
1562
|
+
eventType: "task.cancelled",
|
|
1563
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1564
|
+
taskId,
|
|
1565
|
+
agentId: eventAgentId,
|
|
1566
|
+
sessionScope: eventSessionScope,
|
|
1567
|
+
projectName: eventProjectName
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
if (input.status === "closed" && previousStatus === "needs_review") {
|
|
1571
|
+
recordOrchestrationEventBestEffort({
|
|
1572
|
+
eventType: "review.approved",
|
|
1573
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1574
|
+
taskId,
|
|
1575
|
+
agentId: eventAgentId,
|
|
1576
|
+
reviewer: row.reviewer ? String(row.reviewer) : null,
|
|
1577
|
+
sessionScope: eventSessionScope,
|
|
1578
|
+
projectName: eventProjectName
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
if (previousStatus === "needs_review" && (input.status === "open" || input.status === "in_progress")) {
|
|
1582
|
+
recordOrchestrationEventBestEffort({
|
|
1583
|
+
eventType: "review.rejected",
|
|
1584
|
+
source: "tasks-crud.updateTaskStatus",
|
|
1585
|
+
taskId,
|
|
1586
|
+
agentId: eventAgentId,
|
|
1587
|
+
reviewer: row.reviewer ? String(row.reviewer) : null,
|
|
1588
|
+
sessionScope: eventSessionScope,
|
|
1589
|
+
projectName: eventProjectName,
|
|
1590
|
+
result: input.status
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
try {
|
|
1594
|
+
await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
|
|
1595
|
+
} catch {
|
|
1596
|
+
}
|
|
1597
|
+
if (input.status === "done" || input.status === "needs_review") {
|
|
1598
|
+
try {
|
|
1599
|
+
const { incrementSkillSuccess } = await import("./skill-refinement-MJPOHYD5.js");
|
|
1600
|
+
await incrementSkillSuccess(
|
|
1601
|
+
String(row.assigned_to),
|
|
1602
|
+
row.project_name ? String(row.project_name) : null
|
|
1603
|
+
);
|
|
1604
|
+
} catch {
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
if (input.status === "done" || input.status === "needs_review" || input.status === "cancelled" || input.status === "closed") {
|
|
1608
|
+
try {
|
|
1609
|
+
const { clearQueueForAgent } = await import("./intercom-queue-A6UJEFIF.js");
|
|
1610
|
+
clearQueueForAgent(String(row.assigned_to));
|
|
1611
|
+
} catch {
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
try {
|
|
1615
|
+
await writeCheckpoint({
|
|
1616
|
+
taskId,
|
|
1617
|
+
step: `status_transition:${input.status}`,
|
|
1618
|
+
contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
|
|
1619
|
+
});
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
return { row, taskFile, now, taskId };
|
|
1623
|
+
}
|
|
1624
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
1625
|
+
const client = getClient();
|
|
1626
|
+
const row = await resolveTask(client, taskId);
|
|
1627
|
+
const id = String(row.id);
|
|
1628
|
+
const taskFile = String(row.task_file);
|
|
1629
|
+
const assignedTo = String(row.assigned_to);
|
|
1630
|
+
const assignedBy = String(row.assigned_by);
|
|
1631
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
1632
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
1633
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
1634
|
+
}
|
|
1635
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
1636
|
+
const archPath = path2.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
1637
|
+
try {
|
|
1638
|
+
if (existsSync2(archPath)) return;
|
|
1639
|
+
const template = [
|
|
1640
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
1641
|
+
"",
|
|
1642
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
1643
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
1644
|
+
"",
|
|
1645
|
+
"## Overview",
|
|
1646
|
+
"",
|
|
1647
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
1648
|
+
"",
|
|
1649
|
+
"## Key Components",
|
|
1650
|
+
"",
|
|
1651
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
1652
|
+
"",
|
|
1653
|
+
"## Data Flow",
|
|
1654
|
+
"",
|
|
1655
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
1656
|
+
"",
|
|
1657
|
+
"## Invariants",
|
|
1658
|
+
"",
|
|
1659
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
1660
|
+
"",
|
|
1661
|
+
"## Dependencies",
|
|
1662
|
+
"",
|
|
1663
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
1664
|
+
""
|
|
1665
|
+
].join("\n");
|
|
1666
|
+
await writeFile(archPath, template, "utf-8");
|
|
1667
|
+
} catch {
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
async function ensureGitignoreExe(baseDir) {
|
|
1671
|
+
const gitignorePath = path2.join(baseDir, ".gitignore");
|
|
1672
|
+
try {
|
|
1673
|
+
if (existsSync2(gitignorePath)) {
|
|
1674
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
1675
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
1676
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
1677
|
+
} else {
|
|
1678
|
+
await writeFile(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
1679
|
+
}
|
|
1680
|
+
} catch {
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
async function cleanOrphanedTaskFiles() {
|
|
1684
|
+
const { readdir, unlink, stat } = await import("fs/promises");
|
|
1685
|
+
const EXE_TASKS_DIR = path2.join(os2.homedir(), ".exe-os", "tasks");
|
|
1686
|
+
if (!existsSync2(EXE_TASKS_DIR)) return 0;
|
|
1687
|
+
const client = getClient();
|
|
1688
|
+
let cleaned = 0;
|
|
1689
|
+
async function walkDir(dir) {
|
|
1690
|
+
let entries;
|
|
1691
|
+
try {
|
|
1692
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1693
|
+
} catch {
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
for (const entry of entries) {
|
|
1697
|
+
const fullPath = path2.join(dir, entry.name);
|
|
1698
|
+
if (entry.isDirectory()) {
|
|
1699
|
+
await walkDir(fullPath);
|
|
1700
|
+
} else if (entry.name.endsWith(".md")) {
|
|
1701
|
+
const relativePath = path2.relative(path2.join(os2.homedir(), ".exe-os"), fullPath);
|
|
1702
|
+
try {
|
|
1703
|
+
const result = await client.execute({
|
|
1704
|
+
sql: "SELECT id FROM tasks WHERE task_file = ? LIMIT 1",
|
|
1705
|
+
args: [relativePath]
|
|
1706
|
+
});
|
|
1707
|
+
if (result.rows.length === 0) {
|
|
1708
|
+
const filenameBase = entry.name.replace(/\.md$/, "");
|
|
1709
|
+
const idCheck = await client.execute({
|
|
1710
|
+
sql: "SELECT id FROM tasks WHERE id LIKE ? LIMIT 1",
|
|
1711
|
+
args: [`${filenameBase}%`]
|
|
1712
|
+
});
|
|
1713
|
+
if (idCheck.rows.length === 0) {
|
|
1714
|
+
const fileStat = await stat(fullPath);
|
|
1715
|
+
const ageMs = Date.now() - fileStat.mtimeMs;
|
|
1716
|
+
if (ageMs > 24 * 60 * 60 * 1e3) {
|
|
1717
|
+
await unlink(fullPath);
|
|
1718
|
+
cleaned++;
|
|
1719
|
+
recordOrchestrationEventBestEffort({
|
|
1720
|
+
eventType: "task_file.orphan_cleaned",
|
|
1721
|
+
source: "tasks-crud.cleanOrphanedTaskFiles",
|
|
1722
|
+
severity: "warn",
|
|
1723
|
+
payload: {
|
|
1724
|
+
ageHours: Math.round(ageMs / 36e5),
|
|
1725
|
+
type: "task_file_missing_db"
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
process.stderr.write(`[task-cleanup] Removed orphan task file: ${relativePath}
|
|
1729
|
+
`);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
} catch {
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
await walkDir(EXE_TASKS_DIR);
|
|
1739
|
+
return cleaned;
|
|
1740
|
+
}
|
|
1741
|
+
async function readLatestCheckpoint(agentId) {
|
|
1742
|
+
const client = getClient();
|
|
1743
|
+
const result = await client.execute({
|
|
1744
|
+
sql: `SELECT checkpoint, checkpoint_count FROM tasks
|
|
1745
|
+
WHERE assigned_to = ?
|
|
1746
|
+
AND status IN ('in_progress', 'open')
|
|
1747
|
+
AND checkpoint IS NOT NULL
|
|
1748
|
+
ORDER BY updated_at DESC
|
|
1749
|
+
LIMIT 1`,
|
|
1750
|
+
args: [agentId]
|
|
1751
|
+
});
|
|
1752
|
+
if (result.rows.length === 0) return null;
|
|
1753
|
+
const row = result.rows[0];
|
|
1754
|
+
const checkpointJson = row.checkpoint;
|
|
1755
|
+
if (!checkpointJson) return null;
|
|
1756
|
+
try {
|
|
1757
|
+
return JSON.parse(String(checkpointJson));
|
|
1758
|
+
} catch {
|
|
1759
|
+
return null;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
async function createTaskGroup(input) {
|
|
1763
|
+
const client = getClient();
|
|
1764
|
+
const id = crypto.randomUUID();
|
|
1765
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1766
|
+
await client.execute({
|
|
1767
|
+
sql: `INSERT INTO task_groups (id, title, coordinator, session_scope, project_name, timeout_minutes, on_partial_failure, created_at, updated_at)
|
|
1768
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1769
|
+
args: [
|
|
1770
|
+
id,
|
|
1771
|
+
input.title,
|
|
1772
|
+
input.coordinator,
|
|
1773
|
+
input.sessionScope ?? null,
|
|
1774
|
+
input.projectName ?? null,
|
|
1775
|
+
input.timeoutMinutes ?? 120,
|
|
1776
|
+
input.onPartialFailure ?? "notify",
|
|
1777
|
+
now,
|
|
1778
|
+
now
|
|
1779
|
+
]
|
|
1780
|
+
});
|
|
1781
|
+
recordOrchestrationEventBestEffort({
|
|
1782
|
+
eventType: "task_group.created",
|
|
1783
|
+
source: "tasks-crud.createTaskGroup",
|
|
1784
|
+
agentId: input.coordinator,
|
|
1785
|
+
sessionScope: input.sessionScope ?? null,
|
|
1786
|
+
projectName: input.projectName ?? null,
|
|
1787
|
+
payload: { groupId: id, title: input.title, timeoutMinutes: input.timeoutMinutes ?? 120 }
|
|
1788
|
+
});
|
|
1789
|
+
return {
|
|
1790
|
+
id,
|
|
1791
|
+
title: input.title,
|
|
1792
|
+
coordinator: input.coordinator,
|
|
1793
|
+
sessionScope: input.sessionScope ?? null,
|
|
1794
|
+
projectName: input.projectName ?? null,
|
|
1795
|
+
timeoutMinutes: input.timeoutMinutes ?? 120,
|
|
1796
|
+
onPartialFailure: input.onPartialFailure ?? "notify",
|
|
1797
|
+
barrierFired: false,
|
|
1798
|
+
barrierFiredAt: null,
|
|
1799
|
+
aggregatedResult: null,
|
|
1800
|
+
createdAt: now,
|
|
1801
|
+
updatedAt: now
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
async function getGroupStatus(groupId) {
|
|
1805
|
+
const client = getClient();
|
|
1806
|
+
const groupResult = await client.execute({
|
|
1807
|
+
sql: "SELECT id, title, barrier_fired, timeout_minutes, created_at FROM task_groups WHERE id = ?",
|
|
1808
|
+
args: [groupId]
|
|
1809
|
+
});
|
|
1810
|
+
if (groupResult.rows.length === 0) throw new Error(`Task group not found: ${groupId}`);
|
|
1811
|
+
const group = groupResult.rows[0];
|
|
1812
|
+
const tasksResult = await client.execute({
|
|
1813
|
+
sql: `SELECT status FROM tasks WHERE group_id = ?`,
|
|
1814
|
+
args: [groupId]
|
|
1815
|
+
});
|
|
1816
|
+
const total = tasksResult.rows.length;
|
|
1817
|
+
let completed = 0;
|
|
1818
|
+
let failed = 0;
|
|
1819
|
+
let pending = 0;
|
|
1820
|
+
for (const row of tasksResult.rows) {
|
|
1821
|
+
const status = String(row.status);
|
|
1822
|
+
if (status === "done" || status === "needs_review" || status === "closed") {
|
|
1823
|
+
completed++;
|
|
1824
|
+
} else if (status === "cancelled") {
|
|
1825
|
+
failed++;
|
|
1826
|
+
} else {
|
|
1827
|
+
pending++;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const createdAt = new Date(String(group.created_at)).getTime();
|
|
1831
|
+
const timeoutMs = Number(group.timeout_minutes) * 6e4;
|
|
1832
|
+
const timedOut = Date.now() - createdAt > timeoutMs;
|
|
1833
|
+
return {
|
|
1834
|
+
groupId,
|
|
1835
|
+
title: String(group.title),
|
|
1836
|
+
total,
|
|
1837
|
+
completed,
|
|
1838
|
+
failed,
|
|
1839
|
+
pending,
|
|
1840
|
+
barrierFired: Number(group.barrier_fired) === 1,
|
|
1841
|
+
timedOut
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
async function getGroupResults(groupId) {
|
|
1845
|
+
const client = getClient();
|
|
1846
|
+
const groupCheck = await client.execute({
|
|
1847
|
+
sql: "SELECT id FROM task_groups WHERE id = ?",
|
|
1848
|
+
args: [groupId]
|
|
1849
|
+
});
|
|
1850
|
+
if (groupCheck.rows.length === 0) throw new Error(`Task group not found: ${groupId}`);
|
|
1851
|
+
const result = await client.execute({
|
|
1852
|
+
sql: `SELECT id, title, status, result, structured_result FROM tasks WHERE group_id = ?`,
|
|
1853
|
+
args: [groupId]
|
|
1854
|
+
});
|
|
1855
|
+
return result.rows.map((r) => ({
|
|
1856
|
+
taskId: String(r.id),
|
|
1857
|
+
title: String(r.title),
|
|
1858
|
+
status: String(r.status),
|
|
1859
|
+
result: r.result != null ? String(r.result) : null
|
|
1860
|
+
}));
|
|
1861
|
+
}
|
|
1862
|
+
async function getAggregatedGroupResults(groupId) {
|
|
1863
|
+
const { parseMessage, isTypedMessage } = await import("./typed-messages-TKDGVPHS.js");
|
|
1864
|
+
const client = getClient();
|
|
1865
|
+
const groupCheck = await client.execute({
|
|
1866
|
+
sql: "SELECT id FROM task_groups WHERE id = ?",
|
|
1867
|
+
args: [groupId]
|
|
1868
|
+
});
|
|
1869
|
+
if (groupCheck.rows.length === 0) throw new Error(`Task group not found: ${groupId}`);
|
|
1870
|
+
const result = await client.execute({
|
|
1871
|
+
sql: `SELECT id, title, status, result, structured_result FROM tasks WHERE group_id = ?`,
|
|
1872
|
+
args: [groupId]
|
|
1873
|
+
});
|
|
1874
|
+
const filesChanged = /* @__PURE__ */ new Set();
|
|
1875
|
+
const allCommits = [];
|
|
1876
|
+
const allDecisions = [];
|
|
1877
|
+
const prNumbers = [];
|
|
1878
|
+
const openQuestions = [];
|
|
1879
|
+
let overallTests = "none";
|
|
1880
|
+
let overallBuild = "skip";
|
|
1881
|
+
const tasks = [];
|
|
1882
|
+
for (const r of result.rows) {
|
|
1883
|
+
tasks.push({
|
|
1884
|
+
taskId: String(r.id),
|
|
1885
|
+
title: String(r.title),
|
|
1886
|
+
status: String(r.status),
|
|
1887
|
+
result: r.result != null ? String(r.result) : null
|
|
1888
|
+
});
|
|
1889
|
+
let structuredJson = r.structured_result ? String(r.structured_result) : null;
|
|
1890
|
+
if (!structuredJson && r.result) {
|
|
1891
|
+
const resultStr = String(r.result);
|
|
1892
|
+
const match = resultStr.match(/<!-- STRUCTURED_RESULT:(.*?) -->/);
|
|
1893
|
+
if (match) structuredJson = match[1];
|
|
1894
|
+
}
|
|
1895
|
+
if (structuredJson) {
|
|
1896
|
+
const parsed = parseMessage(structuredJson);
|
|
1897
|
+
if (isTypedMessage(parsed) && parsed.type === "task_result") {
|
|
1898
|
+
const p = parsed.payload;
|
|
1899
|
+
for (const f of p.files_changed) filesChanged.add(f);
|
|
1900
|
+
allCommits.push(...p.commits);
|
|
1901
|
+
allDecisions.push(...p.decisions);
|
|
1902
|
+
openQuestions.push(...p.open_questions);
|
|
1903
|
+
if (p.pr_number) prNumbers.push(p.pr_number);
|
|
1904
|
+
if (p.tests === "fail") overallTests = "fail";
|
|
1905
|
+
else if (p.tests === "pass" && overallTests !== "fail") overallTests = "pass";
|
|
1906
|
+
else if (p.tests === "skip" && overallTests === "none") overallTests = "skip";
|
|
1907
|
+
if (p.build === "fail") overallBuild = "fail";
|
|
1908
|
+
else if (p.build === "pass" && overallBuild !== "fail") overallBuild = "pass";
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return {
|
|
1913
|
+
groupId,
|
|
1914
|
+
totalTasks: result.rows.length,
|
|
1915
|
+
filesChanged: [...filesChanged],
|
|
1916
|
+
allCommits,
|
|
1917
|
+
allDecisions,
|
|
1918
|
+
overallTests,
|
|
1919
|
+
overallBuild,
|
|
1920
|
+
prNumbers,
|
|
1921
|
+
openQuestions,
|
|
1922
|
+
tasks
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
async function checkAndFireBarriers() {
|
|
1926
|
+
const client = getClient();
|
|
1927
|
+
const firedGroups = [];
|
|
1928
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1929
|
+
const activeGroups = await client.execute({
|
|
1930
|
+
sql: `SELECT g.id, g.title, g.coordinator, g.timeout_minutes,
|
|
1931
|
+
g.on_partial_failure, g.session_scope, g.project_name, g.created_at
|
|
1932
|
+
FROM task_groups g
|
|
1933
|
+
WHERE g.barrier_fired = 0`,
|
|
1934
|
+
args: []
|
|
1935
|
+
});
|
|
1936
|
+
for (const group of activeGroups.rows) {
|
|
1937
|
+
const groupId = String(group.id);
|
|
1938
|
+
const coordinator = String(group.coordinator);
|
|
1939
|
+
const onPartialFailure = String(group.on_partial_failure);
|
|
1940
|
+
const sessionScope = group.session_scope != null ? String(group.session_scope) : null;
|
|
1941
|
+
const projectName = group.project_name != null ? String(group.project_name) : null;
|
|
1942
|
+
const taskResult = await client.execute({
|
|
1943
|
+
sql: `SELECT status, id, title, result FROM tasks WHERE group_id = ?`,
|
|
1944
|
+
args: [groupId]
|
|
1945
|
+
});
|
|
1946
|
+
const total = taskResult.rows.length;
|
|
1947
|
+
if (total === 0) continue;
|
|
1948
|
+
const terminal = taskResult.rows.filter((r) => {
|
|
1949
|
+
const s = String(r.status);
|
|
1950
|
+
return s === "done" || s === "needs_review" || s === "closed" || s === "cancelled";
|
|
1951
|
+
});
|
|
1952
|
+
const createdAt = new Date(String(group.created_at)).getTime();
|
|
1953
|
+
const timeoutMs = Number(group.timeout_minutes) * 6e4;
|
|
1954
|
+
const timedOut = Date.now() - createdAt > timeoutMs;
|
|
1955
|
+
const allTerminal = terminal.length === total;
|
|
1956
|
+
if (!allTerminal && !timedOut) continue;
|
|
1957
|
+
const succeeded = terminal.filter((r) => {
|
|
1958
|
+
const s = String(r.status);
|
|
1959
|
+
return s === "done" || s === "needs_review" || s === "closed";
|
|
1960
|
+
}).length;
|
|
1961
|
+
const cancelled = terminal.filter((r) => String(r.status) === "cancelled").length;
|
|
1962
|
+
const pending = total - terminal.length;
|
|
1963
|
+
const taskResults = taskResult.rows.map((r) => ({
|
|
1964
|
+
task_id: String(r.id),
|
|
1965
|
+
title: String(r.title),
|
|
1966
|
+
status: String(r.status),
|
|
1967
|
+
result: r.result != null ? String(r.result) : null
|
|
1968
|
+
}));
|
|
1969
|
+
const aggregatedResult = JSON.stringify({
|
|
1970
|
+
total,
|
|
1971
|
+
succeeded,
|
|
1972
|
+
cancelled,
|
|
1973
|
+
pending,
|
|
1974
|
+
timed_out: timedOut,
|
|
1975
|
+
tasks: taskResults
|
|
1976
|
+
});
|
|
1977
|
+
await client.execute({
|
|
1978
|
+
sql: `UPDATE task_groups SET barrier_fired = 1, barrier_fired_at = ?, aggregated_result = ?, updated_at = ? WHERE id = ?`,
|
|
1979
|
+
args: [now, aggregatedResult, now, groupId]
|
|
1980
|
+
});
|
|
1981
|
+
if (onPartialFailure === "cancel_remaining" && pending > 0) {
|
|
1982
|
+
const nonTerminal = taskResult.rows.filter((r) => {
|
|
1983
|
+
const s = String(r.status);
|
|
1984
|
+
return s !== "done" && s !== "needs_review" && s !== "closed" && s !== "cancelled";
|
|
1985
|
+
});
|
|
1986
|
+
for (const task of nonTerminal) {
|
|
1987
|
+
try {
|
|
1988
|
+
await client.execute({
|
|
1989
|
+
sql: "UPDATE tasks SET status = 'cancelled', result = ?, updated_at = ? WHERE id = ?",
|
|
1990
|
+
args: [`Cancelled by task group barrier (group: ${groupId})`, now, String(task.id)]
|
|
1991
|
+
});
|
|
1992
|
+
recordOrchestrationEventBestEffort({
|
|
1993
|
+
eventType: "task.cancelled",
|
|
1994
|
+
source: "tasks-crud.checkAndFireBarriers",
|
|
1995
|
+
taskId: String(task.id),
|
|
1996
|
+
agentId: null,
|
|
1997
|
+
sessionScope,
|
|
1998
|
+
projectName,
|
|
1999
|
+
payload: { reason: "barrier_cancel_remaining", groupId }
|
|
2000
|
+
});
|
|
2001
|
+
} catch {
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
const eventType = timedOut ? "task_group.timeout" : "task_group.barrier_fired";
|
|
2006
|
+
recordOrchestrationEventBestEffort({
|
|
2007
|
+
eventType,
|
|
2008
|
+
source: "tasks-crud.checkAndFireBarriers",
|
|
2009
|
+
agentId: coordinator,
|
|
2010
|
+
sessionScope,
|
|
2011
|
+
projectName,
|
|
2012
|
+
payload: {
|
|
2013
|
+
groupId,
|
|
2014
|
+
title: String(group.title),
|
|
2015
|
+
total,
|
|
2016
|
+
succeeded,
|
|
2017
|
+
cancelled,
|
|
2018
|
+
pending,
|
|
2019
|
+
timedOut,
|
|
2020
|
+
onPartialFailure
|
|
2021
|
+
}
|
|
2022
|
+
});
|
|
2023
|
+
if (cancelled > 0 || pending > 0) {
|
|
2024
|
+
recordOrchestrationEventBestEffort({
|
|
2025
|
+
eventType: "task_group.partial_failure",
|
|
2026
|
+
source: "tasks-crud.checkAndFireBarriers",
|
|
2027
|
+
agentId: coordinator,
|
|
2028
|
+
sessionScope,
|
|
2029
|
+
projectName,
|
|
2030
|
+
payload: {
|
|
2031
|
+
groupId,
|
|
2032
|
+
total,
|
|
2033
|
+
succeeded,
|
|
2034
|
+
cancelled,
|
|
2035
|
+
pending,
|
|
2036
|
+
onPartialFailure
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
firedGroups.push(groupId);
|
|
2041
|
+
}
|
|
2042
|
+
return firedGroups;
|
|
2043
|
+
}
|
|
2044
|
+
async function checkTaskFileConsistency() {
|
|
2045
|
+
const client = getClient();
|
|
2046
|
+
let mismatches = 0;
|
|
2047
|
+
try {
|
|
2048
|
+
const result = await client.execute({
|
|
2049
|
+
sql: `SELECT id, title, task_file, status, priority, assigned_to, assigned_by,
|
|
2050
|
+
reviewer, context, parent_task_id, created_at, session_scope, project_name
|
|
2051
|
+
FROM tasks
|
|
2052
|
+
WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')
|
|
2053
|
+
AND task_file IS NOT NULL AND task_file != ''`,
|
|
2054
|
+
args: []
|
|
2055
|
+
});
|
|
2056
|
+
for (const row of result.rows) {
|
|
2057
|
+
const taskFile = String(row.task_file ?? "");
|
|
2058
|
+
if (!taskFile) continue;
|
|
2059
|
+
const fullPath = taskFile.startsWith("/") ? taskFile : path2.join(os2.homedir(), ".exe-os", taskFile);
|
|
2060
|
+
if (existsSync2(fullPath)) continue;
|
|
2061
|
+
let repaired = false;
|
|
2062
|
+
try {
|
|
2063
|
+
const dir = path2.dirname(fullPath);
|
|
2064
|
+
if (!existsSync2(dir)) await mkdir(dir, { recursive: true });
|
|
2065
|
+
const mdContent = renderTaskMarkdown({
|
|
2066
|
+
id: String(row.id),
|
|
2067
|
+
title: String(row.title ?? ""),
|
|
2068
|
+
status: String(row.status ?? ""),
|
|
2069
|
+
priority: String(row.priority ?? ""),
|
|
2070
|
+
assignedBy: String(row.assigned_by ?? ""),
|
|
2071
|
+
assignedTo: String(row.assigned_to ?? ""),
|
|
2072
|
+
projectName: String(row.project_name ?? ""),
|
|
2073
|
+
created: String(row.created_at ?? "").split("T")[0] || "",
|
|
2074
|
+
reviewer: row.reviewer ? String(row.reviewer) : row.assigned_by ? String(row.assigned_by) : "",
|
|
2075
|
+
context: String(row.context ?? ""),
|
|
2076
|
+
parentTaskId: row.parent_task_id ? String(row.parent_task_id) : null
|
|
2077
|
+
});
|
|
2078
|
+
await writeFile(fullPath, mdContent, "utf-8");
|
|
2079
|
+
repaired = true;
|
|
2080
|
+
} catch {
|
|
2081
|
+
}
|
|
2082
|
+
if (repaired) {
|
|
2083
|
+
recordOrchestrationEventBestEffort({
|
|
2084
|
+
eventType: "consistency.repaired",
|
|
2085
|
+
source: "tasks-crud.checkTaskFileConsistency",
|
|
2086
|
+
severity: "info",
|
|
2087
|
+
taskId: String(row.id),
|
|
2088
|
+
agentId: row.assigned_to ? String(row.assigned_to) : null,
|
|
2089
|
+
sessionScope: row.session_scope ? String(row.session_scope) : null,
|
|
2090
|
+
projectName: row.project_name ? String(row.project_name) : null,
|
|
2091
|
+
payload: { taskFile, status: String(row.status), type: "db_row_missing_file_regenerated" }
|
|
2092
|
+
});
|
|
2093
|
+
} else {
|
|
2094
|
+
mismatches++;
|
|
2095
|
+
recordOrchestrationEventBestEffort({
|
|
2096
|
+
eventType: "consistency.mismatch",
|
|
2097
|
+
source: "tasks-crud.checkTaskFileConsistency",
|
|
2098
|
+
severity: "warn",
|
|
2099
|
+
taskId: String(row.id),
|
|
2100
|
+
agentId: row.assigned_to ? String(row.assigned_to) : null,
|
|
2101
|
+
sessionScope: row.session_scope ? String(row.session_scope) : null,
|
|
2102
|
+
projectName: row.project_name ? String(row.project_name) : null,
|
|
2103
|
+
payload: { taskFile, status: String(row.status), type: "db_row_missing_file" }
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
} catch {
|
|
2108
|
+
}
|
|
2109
|
+
return mismatches;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/lib/notifications.ts
|
|
2113
|
+
import crypto2 from "crypto";
|
|
2114
|
+
import path3 from "path";
|
|
2115
|
+
import os3 from "os";
|
|
2116
|
+
import {
|
|
2117
|
+
readFileSync as readFileSync2,
|
|
2118
|
+
readdirSync,
|
|
2119
|
+
unlinkSync,
|
|
2120
|
+
existsSync as existsSync3,
|
|
2121
|
+
rmdirSync
|
|
2122
|
+
} from "fs";
|
|
2123
|
+
var CLEANUP_DAYS = 7;
|
|
2124
|
+
async function writeNotification(notification) {
|
|
2125
|
+
try {
|
|
2126
|
+
const client = getClient();
|
|
2127
|
+
const id = crypto2.randomUUID();
|
|
2128
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2129
|
+
const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
|
|
2130
|
+
await client.execute({
|
|
2131
|
+
sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
|
|
2132
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
|
|
2133
|
+
args: [
|
|
2134
|
+
id,
|
|
2135
|
+
notification.agentId,
|
|
2136
|
+
notification.agentRole,
|
|
2137
|
+
notification.event,
|
|
2138
|
+
notification.project,
|
|
2139
|
+
notification.summary,
|
|
2140
|
+
notification.taskFile ?? null,
|
|
2141
|
+
sessionScope,
|
|
2142
|
+
now
|
|
2143
|
+
]
|
|
2144
|
+
});
|
|
2145
|
+
} catch (err) {
|
|
2146
|
+
process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
|
|
2147
|
+
`);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
async function readUnreadNotifications(agentFilter, sessionScope) {
|
|
2151
|
+
try {
|
|
2152
|
+
const client = getClient();
|
|
2153
|
+
const conditions = ["read = 0"];
|
|
2154
|
+
const args = [];
|
|
2155
|
+
const scope = strictSessionScopeFilter(sessionScope ?? null);
|
|
2156
|
+
if (agentFilter) {
|
|
2157
|
+
conditions.push("agent_id = ?");
|
|
2158
|
+
args.push(agentFilter);
|
|
2159
|
+
}
|
|
2160
|
+
const result = await client.execute({
|
|
2161
|
+
sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
|
|
2162
|
+
FROM notifications
|
|
2163
|
+
WHERE ${conditions.join(" AND ")}${scope.sql}
|
|
2164
|
+
ORDER BY created_at ASC`,
|
|
2165
|
+
args: [...args, ...scope.args]
|
|
2166
|
+
});
|
|
2167
|
+
return result.rows.map((r) => ({
|
|
2168
|
+
id: String(r.id),
|
|
2169
|
+
agentId: String(r.agent_id),
|
|
2170
|
+
agentRole: String(r.agent_role),
|
|
2171
|
+
event: String(r.event),
|
|
2172
|
+
project: String(r.project),
|
|
2173
|
+
summary: String(r.summary),
|
|
2174
|
+
taskFile: r.task_file ? String(r.task_file) : void 0,
|
|
2175
|
+
sessionScope: r.session_scope == null ? null : String(r.session_scope),
|
|
2176
|
+
timestamp: String(r.created_at),
|
|
2177
|
+
read: false
|
|
2178
|
+
}));
|
|
2179
|
+
} catch {
|
|
2180
|
+
return [];
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
async function markAsRead(ids, sessionScope) {
|
|
2184
|
+
if (ids.length === 0) return;
|
|
2185
|
+
try {
|
|
2186
|
+
const client = getClient();
|
|
2187
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
2188
|
+
const scope = strictSessionScopeFilter(sessionScope ?? null);
|
|
2189
|
+
await client.execute({
|
|
2190
|
+
sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
|
|
2191
|
+
args: [...ids, ...scope.args]
|
|
2192
|
+
});
|
|
2193
|
+
} catch {
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
async function markAsReadByTaskFile(taskFile, sessionScope) {
|
|
2197
|
+
try {
|
|
2198
|
+
const client = getClient();
|
|
2199
|
+
const scope = strictSessionScopeFilter(sessionScope ?? null);
|
|
2200
|
+
await client.execute({
|
|
2201
|
+
sql: `UPDATE notifications SET read = 1
|
|
2202
|
+
WHERE task_file = ? AND read = 0${scope.sql}`,
|
|
2203
|
+
args: [taskFile, ...scope.args]
|
|
2204
|
+
});
|
|
2205
|
+
} catch {
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
|
|
2209
|
+
try {
|
|
2210
|
+
const client = getClient();
|
|
2211
|
+
const cutoff = new Date(
|
|
2212
|
+
Date.now() - daysOld * 24 * 60 * 60 * 1e3
|
|
2213
|
+
).toISOString();
|
|
2214
|
+
const scope = strictSessionScopeFilter(sessionScope ?? null);
|
|
2215
|
+
const result = await client.execute({
|
|
2216
|
+
sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
|
|
2217
|
+
args: [cutoff, ...scope.args]
|
|
2218
|
+
});
|
|
2219
|
+
return result.rowsAffected;
|
|
2220
|
+
} catch {
|
|
2221
|
+
return 0;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
async function markDoneTaskNotificationsAsRead(sessionScope) {
|
|
2225
|
+
try {
|
|
2226
|
+
const client = getClient();
|
|
2227
|
+
const scope = strictSessionScopeFilter(sessionScope ?? null);
|
|
2228
|
+
const result = await client.execute({
|
|
2229
|
+
sql: `UPDATE notifications SET read = 1
|
|
2230
|
+
WHERE read = 0
|
|
2231
|
+
AND task_file IS NOT NULL
|
|
2232
|
+
${scope.sql}
|
|
2233
|
+
AND task_file IN (
|
|
2234
|
+
SELECT task_file FROM tasks WHERE status IN ('done', 'closed', 'cancelled')
|
|
2235
|
+
)`,
|
|
2236
|
+
args: [...scope.args]
|
|
2237
|
+
});
|
|
2238
|
+
return result.rowsAffected;
|
|
2239
|
+
} catch {
|
|
2240
|
+
return 0;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
function formatNotifications(notifications) {
|
|
2244
|
+
if (notifications.length === 0) return "";
|
|
2245
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2246
|
+
for (const n of notifications) {
|
|
2247
|
+
const key = `${n.agentId}|${n.agentRole}`;
|
|
2248
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
2249
|
+
grouped.get(key).push(n);
|
|
2250
|
+
}
|
|
2251
|
+
const lines = [];
|
|
2252
|
+
lines.push(`## Notifications (${notifications.length} unread)
|
|
2253
|
+
`);
|
|
2254
|
+
for (const [key, items] of grouped) {
|
|
2255
|
+
const [agentId, agentRole] = key.split("|");
|
|
2256
|
+
lines.push(`**${agentId}** (${agentRole}):`);
|
|
2257
|
+
for (const item of items) {
|
|
2258
|
+
const ago = formatTimeAgo(item.timestamp);
|
|
2259
|
+
const icon = eventIcon(item.event);
|
|
2260
|
+
lines.push(`- ${icon} ${item.summary} (${item.project}) \u2014 ${ago}`);
|
|
2261
|
+
}
|
|
2262
|
+
lines.push("");
|
|
2263
|
+
}
|
|
2264
|
+
return lines.join("\n");
|
|
2265
|
+
}
|
|
2266
|
+
async function migrateJsonNotifications() {
|
|
2267
|
+
const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path3.join(os3.homedir(), ".exe-os");
|
|
2268
|
+
const notifDir = path3.join(base, "notifications");
|
|
2269
|
+
if (!existsSync3(notifDir)) return 0;
|
|
2270
|
+
let migrated = 0;
|
|
2271
|
+
try {
|
|
2272
|
+
const files = readdirSync(notifDir).filter((f) => f.endsWith(".json"));
|
|
2273
|
+
if (files.length === 0) return 0;
|
|
2274
|
+
const client = getClient();
|
|
2275
|
+
for (const file of files) {
|
|
2276
|
+
try {
|
|
2277
|
+
const filePath = path3.join(notifDir, file);
|
|
2278
|
+
const data = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
2279
|
+
await client.execute({
|
|
2280
|
+
sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
|
|
2281
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2282
|
+
args: [
|
|
2283
|
+
crypto2.randomUUID(),
|
|
2284
|
+
data.agentId ?? "unknown",
|
|
2285
|
+
data.agentRole ?? "unknown",
|
|
2286
|
+
data.event ?? "session_summary",
|
|
2287
|
+
data.project ?? "unknown",
|
|
2288
|
+
data.summary ?? "",
|
|
2289
|
+
data.taskFile ?? null,
|
|
2290
|
+
null,
|
|
2291
|
+
data.read ? 1 : 0,
|
|
2292
|
+
data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2293
|
+
]
|
|
2294
|
+
});
|
|
2295
|
+
unlinkSync(filePath);
|
|
2296
|
+
migrated++;
|
|
2297
|
+
} catch {
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
try {
|
|
2301
|
+
const remaining = readdirSync(notifDir);
|
|
2302
|
+
if (remaining.length === 0) {
|
|
2303
|
+
rmdirSync(notifDir);
|
|
2304
|
+
}
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
} catch {
|
|
2308
|
+
}
|
|
2309
|
+
return migrated;
|
|
2310
|
+
}
|
|
2311
|
+
function eventIcon(event) {
|
|
2312
|
+
switch (event) {
|
|
2313
|
+
case "task_complete":
|
|
2314
|
+
return "Completed:";
|
|
2315
|
+
case "task_needs_fix":
|
|
2316
|
+
return "Needs fix:";
|
|
2317
|
+
case "session_summary":
|
|
2318
|
+
return "Session:";
|
|
2319
|
+
case "error_spike":
|
|
2320
|
+
return "Errors:";
|
|
2321
|
+
case "orphan_task":
|
|
2322
|
+
return "Orphan:";
|
|
2323
|
+
case "subtasks_complete":
|
|
2324
|
+
return "Subtasks done:";
|
|
2325
|
+
case "task_group_barrier":
|
|
2326
|
+
return "Task group:";
|
|
2327
|
+
case "capacity_relaunch":
|
|
2328
|
+
return "Relaunched:";
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
function formatTimeAgo(timestamp) {
|
|
2332
|
+
const diffMs = Date.now() - new Date(timestamp).getTime();
|
|
2333
|
+
const mins = Math.floor(diffMs / 6e4);
|
|
2334
|
+
if (mins < 1) return "just now";
|
|
2335
|
+
if (mins < 60) return `${mins}m ago`;
|
|
2336
|
+
const hours = Math.floor(mins / 60);
|
|
2337
|
+
if (hours < 24) return `${hours}h ago`;
|
|
2338
|
+
const days = Math.floor(hours / 24);
|
|
2339
|
+
return `${days}d ago`;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// src/lib/tasks-review.ts
|
|
2343
|
+
function formatAge(isoTimestamp) {
|
|
2344
|
+
if (!isoTimestamp) return "";
|
|
2345
|
+
const ms = Date.now() - new Date(isoTimestamp).getTime();
|
|
2346
|
+
if (ms < 0) return "just now";
|
|
2347
|
+
const minutes = Math.floor(ms / 6e4);
|
|
2348
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
2349
|
+
const hours = Math.floor(minutes / 60);
|
|
2350
|
+
if (hours < 24) return `${hours}h ago`;
|
|
2351
|
+
const days = Math.floor(hours / 24);
|
|
2352
|
+
return `${days}d ago`;
|
|
2353
|
+
}
|
|
2354
|
+
function isStale(isoTimestamp) {
|
|
2355
|
+
if (!isoTimestamp) return false;
|
|
2356
|
+
return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
|
|
2357
|
+
}
|
|
2358
|
+
async function countPendingReviews(sessionScope, reviewer) {
|
|
2359
|
+
const extraConditions = reviewer ? ["reviewer = ?"] : void 0;
|
|
2360
|
+
const extraArgs = reviewer ? [reviewer] : void 0;
|
|
2361
|
+
const rows = await queryTaskRows({
|
|
2362
|
+
status: "needs_review",
|
|
2363
|
+
columns: "COUNT(*) as cnt",
|
|
2364
|
+
sessionScope: sessionScope ?? null,
|
|
2365
|
+
strictSession: sessionScope !== void 0,
|
|
2366
|
+
projectName: null,
|
|
2367
|
+
// no project filter for review counts
|
|
2368
|
+
extraConditions,
|
|
2369
|
+
extraArgs,
|
|
2370
|
+
limit: 1,
|
|
2371
|
+
orderBy: "1"
|
|
2372
|
+
});
|
|
2373
|
+
return Number(rows[0]?.cnt) || 0;
|
|
2374
|
+
}
|
|
2375
|
+
async function countNewPendingReviewsSince(sinceIso, sessionScope, reviewer) {
|
|
2376
|
+
const extraConditions = ["updated_at > ?"];
|
|
2377
|
+
const extraArgs = [sinceIso];
|
|
2378
|
+
if (reviewer) {
|
|
2379
|
+
extraConditions.push("reviewer = ?");
|
|
2380
|
+
extraArgs.push(reviewer);
|
|
2381
|
+
}
|
|
2382
|
+
const rows = await queryTaskRows({
|
|
2383
|
+
status: "needs_review",
|
|
2384
|
+
columns: "COUNT(*) as cnt",
|
|
2385
|
+
sessionScope: sessionScope ?? null,
|
|
2386
|
+
strictSession: sessionScope !== void 0,
|
|
2387
|
+
projectName: null,
|
|
2388
|
+
limit: 1,
|
|
2389
|
+
orderBy: "1",
|
|
2390
|
+
extraConditions,
|
|
2391
|
+
extraArgs
|
|
2392
|
+
});
|
|
2393
|
+
return Number(rows[0]?.cnt) || 0;
|
|
2394
|
+
}
|
|
2395
|
+
async function listPendingReviews(limit, sessionScope, projectName, reviewer) {
|
|
2396
|
+
const extraConditions = reviewer ? ["reviewer = ?"] : void 0;
|
|
2397
|
+
const extraArgs = reviewer ? [reviewer] : void 0;
|
|
2398
|
+
const rows = await queryTaskRows({
|
|
2399
|
+
status: "needs_review",
|
|
2400
|
+
columns: "title, assigned_to, reviewer, project_name, updated_at",
|
|
2401
|
+
sessionScope: sessionScope ?? null,
|
|
2402
|
+
strictSession: sessionScope !== void 0,
|
|
2403
|
+
projectName: projectName ?? null,
|
|
2404
|
+
extraConditions,
|
|
2405
|
+
extraArgs,
|
|
2406
|
+
limit,
|
|
2407
|
+
orderBy: "updated_at ASC"
|
|
2408
|
+
});
|
|
2409
|
+
return rows;
|
|
2410
|
+
}
|
|
2411
|
+
async function cleanupOrphanedReviews(sessionScope) {
|
|
2412
|
+
const client = getClient();
|
|
2413
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2414
|
+
const scope = sessionScopeFilter(sessionScope);
|
|
2415
|
+
const r1 = await client.execute({
|
|
2416
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
2417
|
+
WHERE status IN ('open', 'needs_review', 'in_progress')
|
|
2418
|
+
AND assigned_by = 'system'
|
|
2419
|
+
AND title LIKE 'Review:%'
|
|
2420
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))
|
|
2421
|
+
${scope.sql}`,
|
|
2422
|
+
args: [now, ...scope.args]
|
|
2423
|
+
});
|
|
2424
|
+
const r1b = await client.execute({
|
|
2425
|
+
sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
|
|
2426
|
+
WHERE status IN ('open', 'needs_review')
|
|
2427
|
+
AND title LIKE 'Review:%completed%'
|
|
2428
|
+
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')))
|
|
2429
|
+
${scope.sql}`,
|
|
2430
|
+
args: [now, ...scope.args]
|
|
2431
|
+
});
|
|
2432
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
2433
|
+
const r2 = await client.execute({
|
|
2434
|
+
sql: `UPDATE tasks SET status = 'closed', updated_at = ?
|
|
2435
|
+
WHERE status = 'needs_review'
|
|
2436
|
+
AND result IS NOT NULL
|
|
2437
|
+
AND updated_at < ?
|
|
2438
|
+
${scope.sql}`,
|
|
2439
|
+
args: [now, staleThreshold, ...scope.args]
|
|
2440
|
+
});
|
|
2441
|
+
const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
|
|
2442
|
+
if (total > 0) {
|
|
2443
|
+
process.stderr.write(
|
|
2444
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
|
|
2445
|
+
`
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
return total;
|
|
2449
|
+
}
|
|
2450
|
+
function getReviewChecklist(role, agent, taskSlug) {
|
|
2451
|
+
const roleLower = role.toLowerCase();
|
|
2452
|
+
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
2453
|
+
return {
|
|
2454
|
+
lens: "Code Quality (Engineer)",
|
|
2455
|
+
checklist: [
|
|
2456
|
+
"1. Do all tests pass? Any new tests needed?",
|
|
2457
|
+
"2. Is the code clean \u2014 no dead code or unclassified deferred-work markers left?",
|
|
2458
|
+
"3. Does it follow existing patterns and conventions in the codebase?",
|
|
2459
|
+
"4. Any regressions in the test suite?"
|
|
2460
|
+
]
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
if (roleLower === "cto" || roleLower.includes("architect")) {
|
|
2464
|
+
return {
|
|
2465
|
+
lens: "Architecture (CTO)",
|
|
2466
|
+
checklist: [
|
|
2467
|
+
"1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
|
|
2468
|
+
"2. Is it backward compatible? Any breaking changes?",
|
|
2469
|
+
"3. Does it introduce technical debt? Is that debt justified?",
|
|
2470
|
+
"4. Security implications? Any new attack surface?",
|
|
2471
|
+
"5. Does it scale? Performance considerations?",
|
|
2472
|
+
"6. Coordination: does this affect other employees' work or other projects?"
|
|
2473
|
+
]
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
if (roleLower === "coo" || roleLower.includes("operations")) {
|
|
2477
|
+
return {
|
|
2478
|
+
lens: "Strategic (COO)",
|
|
2479
|
+
checklist: [
|
|
2480
|
+
"1. Does this serve the project mission?",
|
|
2481
|
+
"2. Is this the right work at the right time?",
|
|
2482
|
+
"3. Does the architectural assessment make sense for the business?",
|
|
2483
|
+
"4. Any cross-project implications?"
|
|
2484
|
+
]
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
return {
|
|
2488
|
+
lens: "General",
|
|
2489
|
+
checklist: [
|
|
2490
|
+
"1. Read the original task's acceptance criteria",
|
|
2491
|
+
`2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
|
|
2492
|
+
"3. Verify code changes match requirements",
|
|
2493
|
+
"4. Check if tests were added/updated",
|
|
2494
|
+
`5. Look for output files in exe/output/${agent}-${taskSlug}*`
|
|
2495
|
+
]
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
async function createReviewForCompletedTask(row, result, _baseDir, now) {
|
|
2499
|
+
const taskFile = String(row.task_file);
|
|
2500
|
+
const employees = await loadEmployees();
|
|
2501
|
+
const coordinatorName = getCoordinatorName(employees);
|
|
2502
|
+
if (isCoordinatorName(String(row.assigned_to), employees)) return;
|
|
2503
|
+
if (String(row.title).startsWith("Review:")) return;
|
|
2504
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
2505
|
+
if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
|
|
2506
|
+
if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
|
|
2507
|
+
const client = getClient();
|
|
2508
|
+
const agent = String(row.assigned_to);
|
|
2509
|
+
const rawReviewer = row.reviewer || row.assigned_by;
|
|
2510
|
+
const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
|
|
2511
|
+
const currentStatus = String(row.status ?? "");
|
|
2512
|
+
if (currentStatus === "closed") return;
|
|
2513
|
+
const existingResult = String(row.result ?? "");
|
|
2514
|
+
if (existingResult.includes("## Review notes")) return;
|
|
2515
|
+
let reviewerRole = "unknown";
|
|
2516
|
+
try {
|
|
2517
|
+
const emp = getEmployee(employees, reviewer);
|
|
2518
|
+
if (emp) reviewerRole = emp.role;
|
|
2519
|
+
} catch {
|
|
2520
|
+
}
|
|
2521
|
+
const taskTitle = String(row.title);
|
|
2522
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
|
|
2523
|
+
const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
|
|
2524
|
+
process.stderr.write(
|
|
2525
|
+
`[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
|
|
2526
|
+
`
|
|
2527
|
+
);
|
|
2528
|
+
const reviewNotes = [
|
|
2529
|
+
`
|
|
2530
|
+
---
|
|
2531
|
+
## Review notes`,
|
|
2532
|
+
`Review lens: ${lens}`,
|
|
2533
|
+
`Reviewer: **${reviewer}** (${reviewerRole})`,
|
|
2534
|
+
"",
|
|
2535
|
+
"### Checklist",
|
|
2536
|
+
...checklist,
|
|
2537
|
+
"",
|
|
2538
|
+
"### Verdict",
|
|
2539
|
+
"- **Approved:** mark this task as done",
|
|
2540
|
+
"- **Needs work:** re-open with notes"
|
|
2541
|
+
].join("\n");
|
|
2542
|
+
const originalTaskId = String(row.id);
|
|
2543
|
+
const updatedResult = (result ?? "No result summary provided") + reviewNotes;
|
|
2544
|
+
await client.execute({
|
|
2545
|
+
sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
|
|
2546
|
+
WHERE id = ?`,
|
|
2547
|
+
args: [updatedResult, now, originalTaskId]
|
|
2548
|
+
});
|
|
2549
|
+
orgBus.emit({
|
|
2550
|
+
type: "review_created",
|
|
2551
|
+
reviewId: originalTaskId,
|
|
2552
|
+
employee: agent,
|
|
2553
|
+
reviewer,
|
|
2554
|
+
timestamp: now
|
|
2555
|
+
});
|
|
2556
|
+
recordOrchestrationEventBestEffort({
|
|
2557
|
+
eventType: "review.created",
|
|
2558
|
+
source: "tasks-review.annotateForReview",
|
|
2559
|
+
taskId: originalTaskId,
|
|
2560
|
+
agentId: agent,
|
|
2561
|
+
reviewer,
|
|
2562
|
+
sessionScope: row.session_scope ? String(row.session_scope) : null,
|
|
2563
|
+
projectName: row.project_name ? String(row.project_name) : null
|
|
2564
|
+
});
|
|
2565
|
+
await writeNotification({
|
|
2566
|
+
agentId: agent,
|
|
2567
|
+
agentRole: String(row.assigned_to),
|
|
2568
|
+
event: "task_complete",
|
|
2569
|
+
project: String(row.project_name),
|
|
2570
|
+
summary: `completed "${taskTitle}" \u2014 ready for review`,
|
|
2571
|
+
taskFile,
|
|
2572
|
+
sessionScope: row.session_scope ? String(row.session_scope) : null
|
|
2573
|
+
});
|
|
2574
|
+
const autoApprove = false;
|
|
2575
|
+
if (!autoApprove) {
|
|
2576
|
+
try {
|
|
2577
|
+
const taskScope = row.session_scope ? String(row.session_scope) : null;
|
|
2578
|
+
const exeSession = taskScope || getParentExe(getSessionKey());
|
|
2579
|
+
if (exeSession) {
|
|
2580
|
+
sendIntercom(exeSession, { force: true, reason: "completion" });
|
|
2581
|
+
}
|
|
2582
|
+
if (reviewer && reviewer !== coordinatorName && reviewer !== exeSession) {
|
|
2583
|
+
const { employeeSessionName: employeeSessionName2 } = await import("./lib/tmux-routing.js");
|
|
2584
|
+
if (exeSession) {
|
|
2585
|
+
const reviewerSession = employeeSessionName2(reviewer, exeSession);
|
|
2586
|
+
sendIntercom(reviewerSession, { force: true, reason: "completion" });
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
} catch {
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (autoApprove) {
|
|
2593
|
+
process.stderr.write(
|
|
2594
|
+
`[review] Auto-approving "${taskTitle}" (P2 + tests pass)
|
|
2595
|
+
`
|
|
2596
|
+
);
|
|
2597
|
+
try {
|
|
2598
|
+
await updateTaskStatus({ taskId: originalTaskId, status: "closed", skipReviewCreation: true });
|
|
2599
|
+
} catch {
|
|
2600
|
+
await client.execute({
|
|
2601
|
+
sql: "UPDATE tasks SET status = 'closed', updated_at = ? WHERE id = ?",
|
|
2602
|
+
args: [now, originalTaskId]
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
2608
|
+
if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
|
|
2609
|
+
try {
|
|
2610
|
+
const client = getClient();
|
|
2611
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2612
|
+
const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
|
|
2613
|
+
if (parentId) {
|
|
2614
|
+
let cascaded = false;
|
|
2615
|
+
try {
|
|
2616
|
+
await updateTaskStatus({ taskId: parentId, status: "closed", skipReviewCreation: true });
|
|
2617
|
+
cascaded = true;
|
|
2618
|
+
} catch {
|
|
2619
|
+
const result = await client.execute({
|
|
2620
|
+
sql: "UPDATE tasks SET status = 'closed', updated_at = ? WHERE id = ? AND status = 'needs_review'",
|
|
2621
|
+
args: [now, parentId]
|
|
2622
|
+
});
|
|
2623
|
+
cascaded = result.rowsAffected > 0;
|
|
2624
|
+
}
|
|
2625
|
+
if (cascaded) {
|
|
2626
|
+
process.stderr.write(
|
|
2627
|
+
`[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
|
|
2628
|
+
`
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
} else {
|
|
2632
|
+
const fileName = taskFile.split("/").pop() ?? "";
|
|
2633
|
+
const reviewPrefix = fileName.replace(".md", "");
|
|
2634
|
+
const parts = reviewPrefix.split("-");
|
|
2635
|
+
if (parts.length >= 3 && parts[0] === "review") {
|
|
2636
|
+
const agent = parts[1];
|
|
2637
|
+
const slug = parts.slice(2).join("-");
|
|
2638
|
+
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
2639
|
+
const legacyScope = sessionScopeFilter();
|
|
2640
|
+
const result = await client.execute({
|
|
2641
|
+
sql: `UPDATE tasks SET status = 'closed', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'${legacyScope.sql}`,
|
|
2642
|
+
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`, ...legacyScope.args]
|
|
2643
|
+
});
|
|
2644
|
+
if (result.rowsAffected > 0) {
|
|
2645
|
+
process.stderr.write(
|
|
2646
|
+
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
2647
|
+
`
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
} catch (err) {
|
|
2653
|
+
process.stderr.write(
|
|
2654
|
+
`[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
|
|
2655
|
+
`
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
try {
|
|
2659
|
+
const cacheDir = path4.join(EXE_AI_DIR, "session-cache");
|
|
2660
|
+
if (existsSync4(cacheDir)) {
|
|
2661
|
+
for (const f of readdirSync2(cacheDir)) {
|
|
2662
|
+
if (f.startsWith("review-notified-")) {
|
|
2663
|
+
unlinkSync2(path4.join(cacheDir, f));
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
} catch {
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// src/lib/intercom-prompts.ts
|
|
2672
|
+
function formatIntercomPrompt(reason = "nudge") {
|
|
2673
|
+
switch (reason) {
|
|
2674
|
+
case "completion":
|
|
2675
|
+
return "An employee completed work. Run task(action='list', status='needs_review') to review it.";
|
|
2676
|
+
case "message":
|
|
2677
|
+
return "You have a new message. Run message(action='acknowledge') after reading it, and check task(action='list') only if the message references work.";
|
|
2678
|
+
case "notification":
|
|
2679
|
+
return "You have pending notifications. Run task(action='list', status='needs_review') and task(action='list', status='open') to check actionable work.";
|
|
2680
|
+
case "signal":
|
|
2681
|
+
case "nudge":
|
|
2682
|
+
default:
|
|
2683
|
+
return "P0: You have a new task dispatched. Run task(action='list', status='open') to find it. Start immediately.";
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// src/lib/tmux-routing.ts
|
|
2688
|
+
var SPAWN_LOCK_DIR = path5.join(os4.homedir(), ".exe-os", "spawn-locks");
|
|
2689
|
+
function spawnLockPath(sessionName) {
|
|
2690
|
+
return path5.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
2691
|
+
}
|
|
2692
|
+
function isProcessAlive(pid) {
|
|
2693
|
+
try {
|
|
2694
|
+
process.kill(pid, 0);
|
|
2695
|
+
return true;
|
|
2696
|
+
} catch {
|
|
2697
|
+
return false;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
function acquireSpawnLock(sessionName) {
|
|
2701
|
+
if (!existsSync5(SPAWN_LOCK_DIR)) {
|
|
2702
|
+
mkdirSync2(SPAWN_LOCK_DIR, { recursive: true });
|
|
2703
|
+
}
|
|
2704
|
+
const lockFile = spawnLockPath(sessionName);
|
|
2705
|
+
const lockData = JSON.stringify({ pid: process.pid, timestamp: Date.now() });
|
|
2706
|
+
try {
|
|
2707
|
+
const fd = openSync(lockFile, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 420);
|
|
2708
|
+
writeSync(fd, lockData);
|
|
2709
|
+
closeSync(fd);
|
|
2710
|
+
return true;
|
|
2711
|
+
} catch (err) {
|
|
2712
|
+
if (err?.code !== "EEXIST") {
|
|
2713
|
+
return true;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
try {
|
|
2717
|
+
const lock = JSON.parse(readFileSync3(lockFile, "utf8"));
|
|
2718
|
+
const age = Date.now() - lock.timestamp;
|
|
2719
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
2720
|
+
return false;
|
|
2721
|
+
}
|
|
2722
|
+
const tmpName = `${lockFile}.stale-${process.pid}-${Date.now()}`;
|
|
2723
|
+
try {
|
|
2724
|
+
renameSync2(lockFile, tmpName);
|
|
2725
|
+
try {
|
|
2726
|
+
unlinkSync3(tmpName);
|
|
2727
|
+
} catch {
|
|
2728
|
+
}
|
|
2729
|
+
writeFileSync(lockFile, lockData);
|
|
2730
|
+
return true;
|
|
2731
|
+
} catch {
|
|
2732
|
+
return false;
|
|
2733
|
+
}
|
|
2734
|
+
} catch {
|
|
2735
|
+
const tmpName = `${lockFile}.stale-${process.pid}-${Date.now()}`;
|
|
2736
|
+
try {
|
|
2737
|
+
renameSync2(lockFile, tmpName);
|
|
2738
|
+
try {
|
|
2739
|
+
unlinkSync3(tmpName);
|
|
2740
|
+
} catch {
|
|
2741
|
+
}
|
|
2742
|
+
writeFileSync(lockFile, lockData);
|
|
2743
|
+
return true;
|
|
2744
|
+
} catch {
|
|
2745
|
+
return false;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
function releaseSpawnLock(sessionName) {
|
|
2750
|
+
try {
|
|
2751
|
+
unlinkSync3(spawnLockPath(sessionName));
|
|
2752
|
+
} catch {
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
var SESSION_CACHE = path5.join(os4.homedir(), ".exe-os", "session-cache");
|
|
2756
|
+
var BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
2757
|
+
function resolveBehaviorsExporterScript() {
|
|
2758
|
+
try {
|
|
2759
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
2760
|
+
const scriptPath = path5.join(
|
|
2761
|
+
path5.dirname(thisFile),
|
|
2762
|
+
"..",
|
|
2763
|
+
"bin",
|
|
2764
|
+
"exe-export-behaviors.js"
|
|
2765
|
+
);
|
|
2766
|
+
return existsSync5(scriptPath) ? scriptPath : null;
|
|
2767
|
+
} catch {
|
|
2768
|
+
return null;
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
function exportBehaviorsSync(agentId, projectName, sessionKey) {
|
|
2772
|
+
const script = resolveBehaviorsExporterScript();
|
|
2773
|
+
if (!script) return null;
|
|
2774
|
+
try {
|
|
2775
|
+
const output = execFileSync(
|
|
2776
|
+
process.execPath,
|
|
2777
|
+
[script, agentId, projectName, sessionKey],
|
|
2778
|
+
{ encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
|
|
2779
|
+
).trim();
|
|
2780
|
+
return output.length > 0 ? output : null;
|
|
2781
|
+
} catch (err) {
|
|
2782
|
+
process.stderr.write(
|
|
2783
|
+
`[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
2784
|
+
`
|
|
2785
|
+
);
|
|
2786
|
+
return null;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
function getMySession() {
|
|
2790
|
+
return getTransport().getMySession();
|
|
2791
|
+
}
|
|
2792
|
+
function isRootSession(name) {
|
|
2793
|
+
return name.length > 0 && !name.includes("-");
|
|
2794
|
+
}
|
|
2795
|
+
var VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
|
|
2796
|
+
function employeeSessionName(employee, exeSession, instance) {
|
|
2797
|
+
if (!isRootSession(exeSession)) {
|
|
2798
|
+
const root = extractRootExe(exeSession);
|
|
2799
|
+
if (root) {
|
|
2800
|
+
process.stderr.write(
|
|
2801
|
+
`[tmux-routing] WARN: exeSession="${exeSession}" is not a root session, using "${root}" instead
|
|
2802
|
+
`
|
|
2803
|
+
);
|
|
2804
|
+
exeSession = root;
|
|
2805
|
+
} else {
|
|
2806
|
+
throw new Error(
|
|
2807
|
+
`Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
2812
|
+
const name = `${employee}${suffix}-${exeSession}`;
|
|
2813
|
+
if (!VALID_SESSION_NAME.test(name)) {
|
|
2814
|
+
throw new Error(
|
|
2815
|
+
`Invalid session name "${name}" \u2014 must match {agent}-{rootSession} or {agent}{instance}-{rootSession}`
|
|
2816
|
+
);
|
|
2817
|
+
}
|
|
2818
|
+
return name;
|
|
2819
|
+
}
|
|
2820
|
+
function parseParentExe(sessionName, agentId) {
|
|
2821
|
+
const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2822
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
2823
|
+
const match = sessionName.match(regex);
|
|
2824
|
+
return match?.[1] ?? null;
|
|
2825
|
+
}
|
|
2826
|
+
function extractRootExe(name) {
|
|
2827
|
+
if (!name) return null;
|
|
2828
|
+
if (!name.includes("-")) return name;
|
|
2829
|
+
try {
|
|
2830
|
+
const roster = loadEmployeesSync();
|
|
2831
|
+
if (roster.length > 0) {
|
|
2832
|
+
const sortedNames = roster.map((e) => e.name).sort((a, b) => b.length - a.length);
|
|
2833
|
+
for (const agentName of sortedNames) {
|
|
2834
|
+
const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2835
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
2836
|
+
const match = name.match(regex);
|
|
2837
|
+
if (match) {
|
|
2838
|
+
return extractRootExe(match[1]);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
} catch {
|
|
2843
|
+
}
|
|
2844
|
+
const lastDash = name.lastIndexOf("-");
|
|
2845
|
+
if (lastDash < 0) return name;
|
|
2846
|
+
const tail = name.slice(lastDash + 1);
|
|
2847
|
+
return tail.length > 0 ? tail : null;
|
|
2848
|
+
}
|
|
2849
|
+
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
2850
|
+
if (!existsSync5(SESSION_CACHE)) {
|
|
2851
|
+
mkdirSync2(SESSION_CACHE, { recursive: true });
|
|
2852
|
+
}
|
|
2853
|
+
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
2854
|
+
const filePath = path5.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
2855
|
+
atomicWriteJsonSync(filePath, {
|
|
2856
|
+
parentExe: rootExe,
|
|
2857
|
+
dispatchedBy: dispatchedBy || rootExe,
|
|
2858
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
var PARENT_EXE_CACHE_TTL_MS = 4 * 60 * 60 * 1e3;
|
|
2862
|
+
function getParentExe(sessionKey) {
|
|
2863
|
+
try {
|
|
2864
|
+
const data = JSON.parse(readFileSync3(path5.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
2865
|
+
if (data.registeredAt) {
|
|
2866
|
+
const age = Date.now() - new Date(data.registeredAt).getTime();
|
|
2867
|
+
if (age > PARENT_EXE_CACHE_TTL_MS) return null;
|
|
2868
|
+
}
|
|
2869
|
+
return data.parentExe || null;
|
|
2870
|
+
} catch {
|
|
2871
|
+
return null;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
function getDispatchedBy(sessionKey) {
|
|
2875
|
+
try {
|
|
2876
|
+
const data = JSON.parse(readFileSync3(
|
|
2877
|
+
path5.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
2878
|
+
"utf8"
|
|
2879
|
+
));
|
|
2880
|
+
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
2881
|
+
} catch {
|
|
2882
|
+
return null;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
function resolveExeSession() {
|
|
2886
|
+
const mySession = getMySession();
|
|
2887
|
+
const fromSessionName = mySession ? extractRootExe(mySession) ?? null : null;
|
|
2888
|
+
const alsHint = (() => {
|
|
2889
|
+
try {
|
|
2890
|
+
return getAgentContext()?.sessionHint ?? "";
|
|
2891
|
+
} catch {
|
|
2892
|
+
return "";
|
|
2893
|
+
}
|
|
2894
|
+
})();
|
|
2895
|
+
const envHint = process.env.EXE_SESSION_NAME || "";
|
|
2896
|
+
const sessionHintRaw = alsHint || envHint || "";
|
|
2897
|
+
if (sessionHintRaw) {
|
|
2898
|
+
const fromEnv = extractRootExe(sessionHintRaw) ?? sessionHintRaw;
|
|
2899
|
+
if (fromEnv) {
|
|
2900
|
+
if (!mySession) {
|
|
2901
|
+
logSessionResolve({
|
|
2902
|
+
caller: "resolveExeSession",
|
|
2903
|
+
mySession,
|
|
2904
|
+
alsHint,
|
|
2905
|
+
envHint,
|
|
2906
|
+
cacheHit: null,
|
|
2907
|
+
resolved: fromEnv,
|
|
2908
|
+
path: alsHint ? "als_hint (no tmux)" : "env_var (no tmux)"
|
|
2909
|
+
});
|
|
2910
|
+
return fromEnv;
|
|
2911
|
+
}
|
|
2912
|
+
if (fromSessionName && fromEnv !== fromSessionName) {
|
|
2913
|
+
if (alsHint) {
|
|
2914
|
+
process.stderr.write(
|
|
2915
|
+
`[tmux-routing] INFO: ALS hint="${fromEnv}" differs from tmux="${fromSessionName}". Trusting ALS (daemon context).
|
|
2916
|
+
`
|
|
2917
|
+
);
|
|
2918
|
+
logSessionResolve({
|
|
2919
|
+
caller: "resolveExeSession",
|
|
2920
|
+
mySession,
|
|
2921
|
+
alsHint,
|
|
2922
|
+
envHint,
|
|
2923
|
+
cacheHit: null,
|
|
2924
|
+
resolved: fromEnv,
|
|
2925
|
+
path: "als_hint (daemon, tmux ignored)"
|
|
2926
|
+
});
|
|
2927
|
+
return fromEnv;
|
|
2928
|
+
}
|
|
2929
|
+
process.stderr.write(
|
|
2930
|
+
`[tmux-routing] WARN: env hint="${fromEnv}" but tmux says "${fromSessionName}". Trusting tmux.
|
|
2931
|
+
`
|
|
2932
|
+
);
|
|
2933
|
+
logSessionResolve({
|
|
2934
|
+
caller: "resolveExeSession",
|
|
2935
|
+
mySession,
|
|
2936
|
+
alsHint,
|
|
2937
|
+
envHint,
|
|
2938
|
+
cacheHit: null,
|
|
2939
|
+
resolved: fromSessionName,
|
|
2940
|
+
path: "tmux_override (env disagrees, non-daemon)",
|
|
2941
|
+
mismatch: true
|
|
2942
|
+
});
|
|
2943
|
+
process.env.EXE_SESSION_NAME = fromSessionName;
|
|
2944
|
+
} else {
|
|
2945
|
+
logSessionResolve({
|
|
2946
|
+
caller: "resolveExeSession",
|
|
2947
|
+
mySession,
|
|
2948
|
+
alsHint,
|
|
2949
|
+
envHint,
|
|
2950
|
+
cacheHit: null,
|
|
2951
|
+
resolved: fromEnv,
|
|
2952
|
+
path: alsHint ? "als_hint" : "env_var"
|
|
2953
|
+
});
|
|
2954
|
+
return fromEnv;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
if (!mySession) {
|
|
2959
|
+
logSessionResolve({
|
|
2960
|
+
caller: "resolveExeSession",
|
|
2961
|
+
mySession,
|
|
2962
|
+
alsHint,
|
|
2963
|
+
envHint,
|
|
2964
|
+
cacheHit: null,
|
|
2965
|
+
resolved: null,
|
|
2966
|
+
path: "no_tmux_no_hint"
|
|
2967
|
+
});
|
|
2968
|
+
return null;
|
|
2969
|
+
}
|
|
2970
|
+
let candidate = null;
|
|
2971
|
+
let cacheHit = null;
|
|
2972
|
+
try {
|
|
2973
|
+
const key = getSessionKey();
|
|
2974
|
+
const parentExe = getParentExe(key);
|
|
2975
|
+
if (parentExe) {
|
|
2976
|
+
const fromCache = extractRootExe(parentExe) ?? parentExe;
|
|
2977
|
+
cacheHit = fromCache;
|
|
2978
|
+
if (fromSessionName && fromCache !== fromSessionName) {
|
|
2979
|
+
process.stderr.write(
|
|
2980
|
+
`[tmux-routing] WARN: cache says "${fromCache}" but session name says "${fromSessionName}". Trusting session name.
|
|
2981
|
+
`
|
|
2982
|
+
);
|
|
2983
|
+
try {
|
|
2984
|
+
registerParentExe(key, fromSessionName);
|
|
2985
|
+
} catch {
|
|
2986
|
+
}
|
|
2987
|
+
candidate = fromSessionName;
|
|
2988
|
+
} else {
|
|
2989
|
+
candidate = fromCache;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
} catch {
|
|
2993
|
+
}
|
|
2994
|
+
if (!candidate) {
|
|
2995
|
+
candidate = fromSessionName ?? mySession;
|
|
2996
|
+
}
|
|
2997
|
+
if (candidate && isRootSession(candidate)) {
|
|
2998
|
+
try {
|
|
2999
|
+
const transport = getTransport();
|
|
3000
|
+
const liveSessions = transport.listSessions();
|
|
3001
|
+
if (!liveSessions.includes(candidate)) {
|
|
3002
|
+
const liveRoots = liveSessions.filter((s) => isRootSession(s));
|
|
3003
|
+
if (liveRoots.length === 1) {
|
|
3004
|
+
process.stderr.write(
|
|
3005
|
+
`[tmux-routing] WARN: resolved session "${candidate}" is dead. Using live coordinator "${liveRoots[0]}".
|
|
3006
|
+
`
|
|
3007
|
+
);
|
|
3008
|
+
logSessionResolve({
|
|
3009
|
+
caller: "resolveExeSession",
|
|
3010
|
+
mySession,
|
|
3011
|
+
alsHint,
|
|
3012
|
+
envHint,
|
|
3013
|
+
cacheHit,
|
|
3014
|
+
liveRoots,
|
|
3015
|
+
resolved: liveRoots[0],
|
|
3016
|
+
path: "liveness_fallback_single",
|
|
3017
|
+
mismatch: true
|
|
3018
|
+
});
|
|
3019
|
+
return liveRoots[0];
|
|
3020
|
+
} else if (liveRoots.length > 1) {
|
|
3021
|
+
const base = candidate.replace(/\d+$/, "");
|
|
3022
|
+
const match = liveRoots.find((s) => s.startsWith(base));
|
|
3023
|
+
const chosen = match ?? liveRoots[0];
|
|
3024
|
+
process.stderr.write(
|
|
3025
|
+
`[tmux-routing] WARN: resolved session "${candidate}" is dead. ${liveRoots.length} live roots found, using "${chosen}".
|
|
3026
|
+
`
|
|
3027
|
+
);
|
|
3028
|
+
logSessionResolve({
|
|
3029
|
+
caller: "resolveExeSession",
|
|
3030
|
+
mySession,
|
|
3031
|
+
alsHint,
|
|
3032
|
+
envHint,
|
|
3033
|
+
cacheHit,
|
|
3034
|
+
liveRoots,
|
|
3035
|
+
resolved: chosen,
|
|
3036
|
+
path: `liveness_fallback_multi (base="${base}", match=${match ?? "none"})`,
|
|
3037
|
+
mismatch: true
|
|
3038
|
+
});
|
|
3039
|
+
return chosen;
|
|
3040
|
+
}
|
|
3041
|
+
process.stderr.write(
|
|
3042
|
+
`[tmux-routing] WARN: resolved session "${candidate}" is dead and no live coordinator found.
|
|
3043
|
+
`
|
|
3044
|
+
);
|
|
3045
|
+
logSessionResolve({
|
|
3046
|
+
caller: "resolveExeSession",
|
|
3047
|
+
mySession,
|
|
3048
|
+
alsHint,
|
|
3049
|
+
envHint,
|
|
3050
|
+
cacheHit,
|
|
3051
|
+
liveRoots: [],
|
|
3052
|
+
resolved: candidate,
|
|
3053
|
+
path: "liveness_fallback_none (stale candidate)",
|
|
3054
|
+
mismatch: true
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
} catch {
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
logSessionResolve({
|
|
3061
|
+
caller: "resolveExeSession",
|
|
3062
|
+
mySession,
|
|
3063
|
+
alsHint,
|
|
3064
|
+
envHint,
|
|
3065
|
+
cacheHit,
|
|
3066
|
+
resolved: candidate,
|
|
3067
|
+
path: cacheHit ? "cache" : fromSessionName ? "tmux_session_name" : "tmux_raw"
|
|
3068
|
+
});
|
|
3069
|
+
return candidate;
|
|
3070
|
+
}
|
|
3071
|
+
function isEmployeeAlive(sessionName) {
|
|
3072
|
+
return getTransport().isAlive(sessionName);
|
|
3073
|
+
}
|
|
3074
|
+
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
3075
|
+
const base = employeeSessionName(employeeName, exeSession);
|
|
3076
|
+
if (!isAlive(base) && acquireSpawnLock(base)) return 0;
|
|
3077
|
+
for (let i = 2; i <= maxInstances; i++) {
|
|
3078
|
+
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
3079
|
+
if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
|
|
3080
|
+
}
|
|
3081
|
+
return null;
|
|
3082
|
+
}
|
|
3083
|
+
var VERIFY_PANE_LINES = 200;
|
|
3084
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
3085
|
+
const transport = getTransport();
|
|
3086
|
+
if (!transport.isAlive(sessionName)) {
|
|
3087
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
3088
|
+
}
|
|
3089
|
+
let pane;
|
|
3090
|
+
try {
|
|
3091
|
+
pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
|
|
3092
|
+
} catch (err) {
|
|
3093
|
+
return {
|
|
3094
|
+
atCapacity: false,
|
|
3095
|
+
reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
const { isAtCapacity } = await import("./capacity-monitor-IFVRCIM7.js");
|
|
3099
|
+
if (!isAtCapacity(pane)) {
|
|
3100
|
+
return {
|
|
3101
|
+
atCapacity: false,
|
|
3102
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
return {
|
|
3106
|
+
atCapacity: true,
|
|
3107
|
+
reason: "capacity banner matched in recent pane output"
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
var INTERCOM_DEBOUNCE_MS = 3e5;
|
|
3111
|
+
var CODEX_DEBOUNCE_MS = 3e5;
|
|
3112
|
+
var INTERCOM_LOG = path5.join(os4.homedir(), ".exe-os", "intercom.log");
|
|
3113
|
+
var DEBOUNCE_FILE = path5.join(SESSION_CACHE, "intercom-debounce.json");
|
|
3114
|
+
var DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
3115
|
+
function readDebounceState() {
|
|
3116
|
+
try {
|
|
3117
|
+
if (!existsSync5(DEBOUNCE_FILE)) return {};
|
|
3118
|
+
const raw = JSON.parse(readFileSync3(DEBOUNCE_FILE, "utf8"));
|
|
3119
|
+
const state = {};
|
|
3120
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
3121
|
+
if (typeof val === "number") {
|
|
3122
|
+
state[key] = { lastSent: val, pending: 0 };
|
|
3123
|
+
} else if (val && typeof val === "object" && "lastSent" in val) {
|
|
3124
|
+
state[key] = val;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
return state;
|
|
3128
|
+
} catch {
|
|
3129
|
+
return {};
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
function writeDebounceState(state) {
|
|
3133
|
+
try {
|
|
3134
|
+
if (!existsSync5(SESSION_CACHE)) mkdirSync2(SESSION_CACHE, { recursive: true });
|
|
3135
|
+
atomicWriteJsonSync(DEBOUNCE_FILE, state);
|
|
3136
|
+
} catch {
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
function isDebounced(targetSession) {
|
|
3140
|
+
const state = readDebounceState();
|
|
3141
|
+
const entry = state[targetSession];
|
|
3142
|
+
const lastSent = entry?.lastSent ?? 0;
|
|
3143
|
+
const agentName = baseAgentName(targetSession.split("-")[0] ?? "");
|
|
3144
|
+
let dbRuntime = "claude";
|
|
3145
|
+
try {
|
|
3146
|
+
const diPath = path5.join(SESSION_CACHE, `dispatch-info-${targetSession}.json`);
|
|
3147
|
+
if (existsSync5(diPath)) {
|
|
3148
|
+
const di = JSON.parse(readFileSync3(diPath, "utf-8"));
|
|
3149
|
+
if (di.runtime) dbRuntime = di.runtime;
|
|
3150
|
+
} else {
|
|
3151
|
+
dbRuntime = getAgentRuntime(agentName).runtime;
|
|
3152
|
+
}
|
|
3153
|
+
} catch {
|
|
3154
|
+
dbRuntime = getAgentRuntime(agentName).runtime;
|
|
3155
|
+
}
|
|
3156
|
+
const debounceMs = dbRuntime === "codex" || dbRuntime === "opencode" ? CODEX_DEBOUNCE_MS : INTERCOM_DEBOUNCE_MS;
|
|
3157
|
+
if (Date.now() - lastSent < debounceMs) {
|
|
3158
|
+
if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
|
|
3159
|
+
state[targetSession].pending++;
|
|
3160
|
+
writeDebounceState(state);
|
|
3161
|
+
return true;
|
|
3162
|
+
}
|
|
3163
|
+
return false;
|
|
3164
|
+
}
|
|
3165
|
+
function recordDebounce(targetSession) {
|
|
3166
|
+
const state = readDebounceState();
|
|
3167
|
+
const batched = state[targetSession]?.pending ?? 0;
|
|
3168
|
+
state[targetSession] = { lastSent: Date.now(), pending: 0 };
|
|
3169
|
+
const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
|
|
3170
|
+
for (const key of Object.keys(state)) {
|
|
3171
|
+
if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
|
|
3172
|
+
}
|
|
3173
|
+
writeDebounceState(state);
|
|
3174
|
+
return batched;
|
|
3175
|
+
}
|
|
3176
|
+
function logIntercom(msg, meta) {
|
|
3177
|
+
const metaStr = meta ? ` | caller=${meta.caller ?? "?"} task=${meta.task ?? "none"} trigger=${meta.trigger ?? "?"}` : "";
|
|
3178
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}${metaStr}
|
|
3179
|
+
`;
|
|
3180
|
+
process.stderr.write(`[intercom] ${msg}${metaStr}
|
|
3181
|
+
`);
|
|
3182
|
+
try {
|
|
3183
|
+
appendFileSync2(INTERCOM_LOG, line);
|
|
3184
|
+
} catch {
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
var BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
|
|
3188
|
+
function getSessionState(sessionName) {
|
|
3189
|
+
const transport = getTransport();
|
|
3190
|
+
if (!transport.isAlive(sessionName)) return "offline";
|
|
3191
|
+
try {
|
|
3192
|
+
const pane = transport.capturePane(sessionName, 5);
|
|
3193
|
+
if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
|
|
3194
|
+
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
3195
|
+
return "no_claude";
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
if (/Running…/.test(pane)) return "tool";
|
|
3199
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
3200
|
+
return "idle";
|
|
3201
|
+
} catch {
|
|
3202
|
+
return "offline";
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
function isSessionBusy(sessionName) {
|
|
3206
|
+
const state = getSessionState(sessionName);
|
|
3207
|
+
return state === "thinking" || state === "tool";
|
|
3208
|
+
}
|
|
3209
|
+
function isExeSession(sessionName) {
|
|
3210
|
+
const matchesBaseWithInstance = (baseName) => sessionName === baseName || sessionName.startsWith(baseName) && /^\d+$/.test(sessionName.slice(baseName.length));
|
|
3211
|
+
const coordinatorName = getCoordinatorName();
|
|
3212
|
+
return matchesBaseWithInstance(coordinatorName);
|
|
3213
|
+
}
|
|
3214
|
+
function sendIntercom(targetSession, opts) {
|
|
3215
|
+
const transport = getTransport();
|
|
3216
|
+
const startedAt = Date.now();
|
|
3217
|
+
const reason = opts?.reason ?? "nudge";
|
|
3218
|
+
const rawAgent = targetSession.split("-")[0] ?? targetSession;
|
|
3219
|
+
const eventAgent = baseAgentName(rawAgent);
|
|
3220
|
+
const eventSessionScope = extractRootExe(targetSession);
|
|
3221
|
+
const recordResult = (result, errorCode) => {
|
|
3222
|
+
recordOrchestrationEventBestEffort({
|
|
3223
|
+
eventType: "tmux.nudge.completed",
|
|
3224
|
+
source: "tmux-routing.sendIntercom",
|
|
3225
|
+
severity: result === "failed" ? "warn" : "info",
|
|
3226
|
+
agentId: eventAgent,
|
|
3227
|
+
sessionScope: eventSessionScope,
|
|
3228
|
+
tmuxSession: targetSession,
|
|
3229
|
+
durationMs: Date.now() - startedAt,
|
|
3230
|
+
result,
|
|
3231
|
+
errorCode: errorCode ?? null,
|
|
3232
|
+
payload: { reason, force: Boolean(opts?.force) }
|
|
3233
|
+
});
|
|
3234
|
+
};
|
|
3235
|
+
recordOrchestrationEventBestEffort({
|
|
3236
|
+
eventType: "tmux.nudge.attempted",
|
|
3237
|
+
source: "tmux-routing.sendIntercom",
|
|
3238
|
+
agentId: eventAgent,
|
|
3239
|
+
sessionScope: eventSessionScope,
|
|
3240
|
+
tmuxSession: targetSession,
|
|
3241
|
+
payload: { reason, force: Boolean(opts?.force) }
|
|
3242
|
+
});
|
|
3243
|
+
const isVitest = process.env.VITEST === "true" || process.env.NODE_ENV === "test";
|
|
3244
|
+
const transportName = transport.constructor?.name ?? "";
|
|
3245
|
+
if (isVitest && transportName === "TmuxTransport" && process.env.EXE_TEST_ALLOW_REAL_INTERCOM !== "1") {
|
|
3246
|
+
logIntercom(`SUPPRESSED_TEST \u2192 ${targetSession} (real tmux intercom disabled under Vitest)`);
|
|
3247
|
+
recordResult("debounced", "test_real_tmux_suppressed");
|
|
3248
|
+
return "debounced";
|
|
3249
|
+
}
|
|
3250
|
+
try {
|
|
3251
|
+
const callerScope = resolveExeSession();
|
|
3252
|
+
if (callerScope && isExeSession(callerScope) && targetSession.includes("-")) {
|
|
3253
|
+
const targetScope = extractRootExe(targetSession);
|
|
3254
|
+
if (targetScope && targetScope !== callerScope && isExeSession(targetScope)) {
|
|
3255
|
+
logIntercom(`BLOCKED \u2192 ${targetSession} (scope violation: caller=${callerScope}, target=${targetScope})`);
|
|
3256
|
+
process.stderr.write(
|
|
3257
|
+
`[intercom] BLOCKED: cross-session intercom from ${callerScope} to ${targetSession}. Session isolation enforced.
|
|
3258
|
+
`
|
|
3259
|
+
);
|
|
3260
|
+
recordOrchestrationEventBestEffort({
|
|
3261
|
+
eventType: "session.scope_violation",
|
|
3262
|
+
source: "tmux-routing.sendIntercom",
|
|
3263
|
+
severity: "warn",
|
|
3264
|
+
agentId: eventAgent,
|
|
3265
|
+
sessionScope: targetScope,
|
|
3266
|
+
tmuxSession: targetSession,
|
|
3267
|
+
result: "blocked",
|
|
3268
|
+
payload: { callerScope }
|
|
3269
|
+
});
|
|
3270
|
+
recordResult("failed", "scope_violation");
|
|
3271
|
+
return "failed";
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
} catch {
|
|
3275
|
+
}
|
|
3276
|
+
if (!opts?.force && isDebounced(targetSession)) {
|
|
3277
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
|
|
3278
|
+
recordResult("debounced");
|
|
3279
|
+
return "debounced";
|
|
3280
|
+
}
|
|
3281
|
+
try {
|
|
3282
|
+
const sessions = transport.listSessions();
|
|
3283
|
+
if (!sessions.includes(targetSession)) {
|
|
3284
|
+
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
3285
|
+
recordResult("failed", "session_not_found");
|
|
3286
|
+
return "failed";
|
|
3287
|
+
}
|
|
3288
|
+
const sessionState = getSessionState(targetSession);
|
|
3289
|
+
if (sessionState === "no_claude") {
|
|
3290
|
+
queueIntercom(targetSession, "claude not running in session", reason);
|
|
3291
|
+
const batched2 = recordDebounce(targetSession);
|
|
3292
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
3293
|
+
recordResult("queued", "no_claude");
|
|
3294
|
+
return "queued";
|
|
3295
|
+
}
|
|
3296
|
+
if (!opts?.force && (sessionState === "thinking" || sessionState === "tool")) {
|
|
3297
|
+
queueIntercom(targetSession, "session busy at send time", reason);
|
|
3298
|
+
const batched2 = recordDebounce(targetSession);
|
|
3299
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
|
|
3300
|
+
recordResult("queued", "session_busy");
|
|
3301
|
+
return "queued";
|
|
3302
|
+
}
|
|
3303
|
+
if (sessionState !== "idle") {
|
|
3304
|
+
try {
|
|
3305
|
+
const rawAgent2 = targetSession.split("-")[0] ?? targetSession;
|
|
3306
|
+
const agent = baseAgentName(rawAgent2);
|
|
3307
|
+
const markerPath = path5.join(SESSION_CACHE, `current-task-${agent}.json`);
|
|
3308
|
+
if (existsSync5(markerPath)) {
|
|
3309
|
+
logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
|
|
3310
|
+
recordResult("debounced", "in_progress_marker");
|
|
3311
|
+
return "debounced";
|
|
3312
|
+
}
|
|
3313
|
+
} catch {
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
if (transport.isPaneInCopyMode(targetSession)) {
|
|
3317
|
+
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
3318
|
+
transport.sendKeys(targetSession, "q");
|
|
3319
|
+
}
|
|
3320
|
+
const msg = formatIntercomPrompt(reason);
|
|
3321
|
+
transport.sendKeysLiteral(targetSession, msg);
|
|
3322
|
+
const batched = recordDebounce(targetSession);
|
|
3323
|
+
logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
|
|
3324
|
+
recordResult("delivered");
|
|
3325
|
+
return "delivered";
|
|
3326
|
+
} catch (e) {
|
|
3327
|
+
process.stderr.write("[tmux-routing] sendIntercom to " + targetSession + ": " + (e instanceof Error ? e.message : String(e)) + "\n");
|
|
3328
|
+
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
3329
|
+
recordResult("failed", e instanceof Error ? e.name : "send_error");
|
|
3330
|
+
return "failed";
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
function notifyParentExe(sessionKey) {
|
|
3334
|
+
const target = getDispatchedBy(sessionKey);
|
|
3335
|
+
if (!target) {
|
|
3336
|
+
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
3337
|
+
`);
|
|
3338
|
+
return false;
|
|
3339
|
+
}
|
|
3340
|
+
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
3341
|
+
`);
|
|
3342
|
+
const result = sendIntercom(target, { reason: "completion" });
|
|
3343
|
+
if (result === "failed") {
|
|
3344
|
+
const rootExe = resolveExeSession();
|
|
3345
|
+
if (rootExe && rootExe !== target) {
|
|
3346
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root coordinator session ${rootExe}
|
|
3347
|
+
`);
|
|
3348
|
+
const fallback = sendIntercom(rootExe, { reason: "completion" });
|
|
3349
|
+
return fallback !== "failed";
|
|
3350
|
+
}
|
|
3351
|
+
return false;
|
|
3352
|
+
}
|
|
3353
|
+
return true;
|
|
3354
|
+
}
|
|
3355
|
+
function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
|
|
3356
|
+
const transport = getTransport();
|
|
3357
|
+
try {
|
|
3358
|
+
const sessions = transport.listSessions();
|
|
3359
|
+
if (!sessions.includes(coordinatorSession)) return false;
|
|
3360
|
+
try {
|
|
3361
|
+
const pending = countPendingReviews(coordinatorSession, getCoordinatorName());
|
|
3362
|
+
if (pending instanceof Promise) {
|
|
3363
|
+
pending.then((count) => {
|
|
3364
|
+
if (count > 0) {
|
|
3365
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending \u2014 sending intercom)`);
|
|
3366
|
+
sendIntercom(coordinatorSession, { force: true, reason: "completion" });
|
|
3367
|
+
}
|
|
3368
|
+
}).catch(() => {
|
|
3369
|
+
});
|
|
3370
|
+
return true;
|
|
3371
|
+
}
|
|
3372
|
+
} catch {
|
|
3373
|
+
}
|
|
3374
|
+
logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}") \u2014 review count unavailable, sending intercom as fallback`);
|
|
3375
|
+
sendIntercom(coordinatorSession, { force: true, reason: "completion" });
|
|
3376
|
+
return true;
|
|
3377
|
+
} catch (e) {
|
|
3378
|
+
process.stderr.write("[tmux-routing] notifyCoordinatorTaskCompletion for " + coordinatorSession + ": " + (e instanceof Error ? e.message : String(e)) + "\n");
|
|
3379
|
+
return false;
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
3383
|
+
if (isCoordinatorName(employeeName)) {
|
|
3384
|
+
return { status: "failed", sessionName: "", error: "The coordinator is not a dispatchable employee" };
|
|
3385
|
+
}
|
|
3386
|
+
try {
|
|
3387
|
+
assertEmployeeLimitSync();
|
|
3388
|
+
} catch (err) {
|
|
3389
|
+
if (err instanceof PlanLimitError) {
|
|
3390
|
+
return { status: "failed", sessionName: "", error: err.message };
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
try {
|
|
3394
|
+
const MAX_CONCURRENT = Number(process.env.EXE_MAX_SESSIONS) || 50;
|
|
3395
|
+
const liveSessions = listTmuxSessions().filter((s) => !isExeSession(s));
|
|
3396
|
+
if (liveSessions.length >= MAX_CONCURRENT) {
|
|
3397
|
+
return {
|
|
3398
|
+
status: "failed",
|
|
3399
|
+
sessionName: "",
|
|
3400
|
+
error: `Global session cap reached (${liveSessions.length}/${MAX_CONCURRENT}). Set EXE_MAX_SESSIONS to increase.`
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
} catch {
|
|
3404
|
+
}
|
|
3405
|
+
if (employeeName.includes("-")) {
|
|
3406
|
+
const bare = employeeName.split("-")[0].replace(/\d+$/, "");
|
|
3407
|
+
return {
|
|
3408
|
+
status: "failed",
|
|
3409
|
+
sessionName: "",
|
|
3410
|
+
error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
if (!isRootSession(exeSession)) {
|
|
3414
|
+
const root = extractRootExe(exeSession);
|
|
3415
|
+
if (root) {
|
|
3416
|
+
process.stderr.write(
|
|
3417
|
+
`[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root session). Auto-correcting to "${root}".
|
|
3418
|
+
`
|
|
3419
|
+
);
|
|
3420
|
+
exeSession = root;
|
|
3421
|
+
} else {
|
|
3422
|
+
return {
|
|
3423
|
+
status: "failed",
|
|
3424
|
+
sessionName: "",
|
|
3425
|
+
error: `Invalid coordinator session "${exeSession}" \u2014 contains a dash but no recognizable root session. Pass a root session name.`
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
let effectiveInstance = opts?.instance;
|
|
3430
|
+
if (effectiveInstance === void 0 && opts?.autoInstance) {
|
|
3431
|
+
const free = findFreeInstance(
|
|
3432
|
+
employeeName,
|
|
3433
|
+
exeSession,
|
|
3434
|
+
opts.maxAutoInstances ?? 10
|
|
3435
|
+
);
|
|
3436
|
+
if (free === null) {
|
|
3437
|
+
return {
|
|
3438
|
+
status: "failed",
|
|
3439
|
+
sessionName: employeeSessionName(employeeName, exeSession),
|
|
3440
|
+
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
effectiveInstance = free === 0 ? void 0 : free;
|
|
3444
|
+
if (free !== null) {
|
|
3445
|
+
recordOrchestrationEventBestEffort({
|
|
3446
|
+
eventType: "worker.instance.selected",
|
|
3447
|
+
source: "tmux-routing.ensureEmployee",
|
|
3448
|
+
agentId: employeeName,
|
|
3449
|
+
sessionScope: exeSession,
|
|
3450
|
+
instanceId: String(free),
|
|
3451
|
+
payload: { maxInstances: opts.maxAutoInstances ?? 10 }
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
3456
|
+
if (!opts?.autoInstance) {
|
|
3457
|
+
if (!acquireSpawnLock(sessionName)) {
|
|
3458
|
+
process.stderr.write(
|
|
3459
|
+
`[ensureEmployee] spawn lock held for ${sessionName} \u2014 another process is already spawning, skipping duplicate spawn
|
|
3460
|
+
`
|
|
3461
|
+
);
|
|
3462
|
+
return { status: "intercom_sent", sessionName };
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
if (isEmployeeAlive(sessionName)) {
|
|
3466
|
+
if (!opts?.autoInstance) releaseSpawnLock(sessionName);
|
|
3467
|
+
const result2 = sendIntercom(sessionName, { force: true, reason: "signal" });
|
|
3468
|
+
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
3469
|
+
return { status: "intercom_sent", sessionName };
|
|
3470
|
+
}
|
|
3471
|
+
if (result2 === "delivered") {
|
|
3472
|
+
return { status: "intercom_unprocessed", sessionName };
|
|
3473
|
+
}
|
|
3474
|
+
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
3475
|
+
}
|
|
3476
|
+
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
3477
|
+
if (!spawnOpts.cwd) {
|
|
3478
|
+
const COORDINATOR_ROLES = /* @__PURE__ */ new Set(["COO", "CTO", "CMO"]);
|
|
3479
|
+
try {
|
|
3480
|
+
const roster = loadEmployeesSync();
|
|
3481
|
+
const emp = roster.find((e) => baseAgentName(e.name) === baseAgentName(employeeName));
|
|
3482
|
+
if (emp && !COORDINATOR_ROLES.has(emp.role)) {
|
|
3483
|
+
const wtPath = ensureWorktree(projectDir, employeeName, effectiveInstance);
|
|
3484
|
+
if (wtPath) {
|
|
3485
|
+
spawnOpts.cwd = wtPath;
|
|
3486
|
+
process.stderr.write(`[tmux-routing] worktree isolation: ${employeeName} \u2192 ${wtPath}
|
|
3487
|
+
`);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
} catch (e) {
|
|
3491
|
+
process.stderr.write(`[tmux-routing] worktree setup failed for ${employeeName}: ${e instanceof Error ? e.message : String(e)}
|
|
3492
|
+
`);
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
3496
|
+
if (result.error) {
|
|
3497
|
+
return { status: "failed", sessionName, error: result.error };
|
|
3498
|
+
}
|
|
3499
|
+
return { status: "spawned", sessionName };
|
|
3500
|
+
}
|
|
3501
|
+
function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
3502
|
+
const transport = getTransport();
|
|
3503
|
+
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
3504
|
+
const spawnStartedAt = Date.now();
|
|
3505
|
+
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
3506
|
+
const logDir = path5.join(os4.homedir(), ".exe-os", "session-logs");
|
|
3507
|
+
const logFile = path5.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
3508
|
+
recordOrchestrationEventBestEffort({
|
|
3509
|
+
eventType: "tmux.spawn.attempted",
|
|
3510
|
+
source: "tmux-routing.spawnEmployee",
|
|
3511
|
+
agentId: employeeName,
|
|
3512
|
+
sessionScope: exeSession,
|
|
3513
|
+
instanceId: opts?.instance != null ? String(opts.instance) : null,
|
|
3514
|
+
tmuxSession: sessionName,
|
|
3515
|
+
runtime: opts?.runtimeOverride ?? null,
|
|
3516
|
+
payload: { hasCwdOverride: Boolean(opts?.cwd), autoInstance: Boolean(opts?.autoInstance) }
|
|
3517
|
+
});
|
|
3518
|
+
if (!existsSync5(logDir)) {
|
|
3519
|
+
mkdirSync2(logDir, { recursive: true });
|
|
3520
|
+
}
|
|
3521
|
+
transport.kill(sessionName);
|
|
3522
|
+
let cleanupSuffix = "";
|
|
3523
|
+
try {
|
|
3524
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
3525
|
+
const cleanupScript = path5.join(path5.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
3526
|
+
if (existsSync5(cleanupScript)) {
|
|
3527
|
+
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
3528
|
+
}
|
|
3529
|
+
} catch {
|
|
3530
|
+
}
|
|
3531
|
+
try {
|
|
3532
|
+
const claudeJsonPath = path5.join(os4.homedir(), ".claude.json");
|
|
3533
|
+
let claudeJson = {};
|
|
3534
|
+
try {
|
|
3535
|
+
claudeJson = JSON.parse(readFileSync3(claudeJsonPath, "utf8"));
|
|
3536
|
+
} catch {
|
|
3537
|
+
}
|
|
3538
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
3539
|
+
const projects = claudeJson.projects;
|
|
3540
|
+
const trustDir = opts?.cwd ?? projectDir;
|
|
3541
|
+
if (!projects[trustDir]) projects[trustDir] = {};
|
|
3542
|
+
projects[trustDir].hasTrustDialogAccepted = true;
|
|
3543
|
+
atomicWriteSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
3544
|
+
} catch {
|
|
3545
|
+
}
|
|
3546
|
+
try {
|
|
3547
|
+
const settingsDir = path5.join(os4.homedir(), ".claude", "projects");
|
|
3548
|
+
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
3549
|
+
const projSettingsDir = path5.join(settingsDir, normalizedKey);
|
|
3550
|
+
const settingsPath = path5.join(projSettingsDir, "settings.json");
|
|
3551
|
+
let settings = {};
|
|
3552
|
+
try {
|
|
3553
|
+
settings = JSON.parse(readFileSync3(settingsPath, "utf8"));
|
|
3554
|
+
} catch {
|
|
3555
|
+
}
|
|
3556
|
+
const perms = settings.permissions ?? {};
|
|
3557
|
+
const allow = perms.allow ?? [];
|
|
3558
|
+
const toolNames = [
|
|
3559
|
+
"recall_my_memory",
|
|
3560
|
+
"store_memory",
|
|
3561
|
+
"create_task",
|
|
3562
|
+
"update_task",
|
|
3563
|
+
"list_tasks",
|
|
3564
|
+
"get_task",
|
|
3565
|
+
"ask_team_memory",
|
|
3566
|
+
"store_behavior",
|
|
3567
|
+
"get_identity",
|
|
3568
|
+
"send_message"
|
|
3569
|
+
];
|
|
3570
|
+
const requiredTools = expandDualPrefixTools(toolNames);
|
|
3571
|
+
let changed = false;
|
|
3572
|
+
for (const tool of requiredTools) {
|
|
3573
|
+
if (!allow.includes(tool)) {
|
|
3574
|
+
allow.push(tool);
|
|
3575
|
+
changed = true;
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
if (changed) {
|
|
3579
|
+
perms.allow = allow;
|
|
3580
|
+
settings.permissions = perms;
|
|
3581
|
+
mkdirSync2(projSettingsDir, { recursive: true });
|
|
3582
|
+
atomicWriteSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3583
|
+
}
|
|
3584
|
+
} catch {
|
|
3585
|
+
}
|
|
3586
|
+
const spawnCwd = opts?.cwd ?? projectDir;
|
|
3587
|
+
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
3588
|
+
const baseRtConfig = getAgentRuntime(employeeName);
|
|
3589
|
+
const agentRtConfig = {
|
|
3590
|
+
...baseRtConfig,
|
|
3591
|
+
...opts?.runtimeOverride ? { runtime: opts.runtimeOverride } : {},
|
|
3592
|
+
...opts?.modelOverride ? { model: opts.modelOverride } : {}
|
|
3593
|
+
};
|
|
3594
|
+
const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
|
|
3595
|
+
const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
|
|
3596
|
+
const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
3597
|
+
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
3598
|
+
let legacyFallbackWarned = false;
|
|
3599
|
+
if (!useExeAgent && !useBinSymlink) {
|
|
3600
|
+
const identityPath = path5.join(
|
|
3601
|
+
os4.homedir(),
|
|
3602
|
+
".exe-os",
|
|
3603
|
+
"identity",
|
|
3604
|
+
`${employeeName}.md`
|
|
3605
|
+
);
|
|
3606
|
+
_resetCcAgentSupportCache();
|
|
3607
|
+
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
3608
|
+
if (hasAgentFlag) {
|
|
3609
|
+
const symlink = ensureAgentSymlink(employeeName);
|
|
3610
|
+
if (symlink.action === "conflict") {
|
|
3611
|
+
process.stderr.write(
|
|
3612
|
+
`[tmux-routing] WARN: respecting customer-owned ~/.claude/agents/${employeeName}.md (${symlink.conflict}); not overwriting or bypassing it.
|
|
3613
|
+
`
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
} else if (existsSync5(identityPath)) {
|
|
3617
|
+
legacyFallbackWarned = true;
|
|
3618
|
+
}
|
|
3619
|
+
const behaviorsFile = exportBehaviorsSync(
|
|
3620
|
+
employeeName,
|
|
3621
|
+
path5.basename(spawnCwd),
|
|
3622
|
+
sessionName
|
|
3623
|
+
);
|
|
3624
|
+
void behaviorsFile;
|
|
3625
|
+
}
|
|
3626
|
+
if (legacyFallbackWarned) {
|
|
3627
|
+
process.stderr.write(
|
|
3628
|
+
`[tmux-routing] claude --agent not supported by installed CC. Falling back to --append-system-prompt-file for ${employeeName}. Upgrade Claude Code to enable native --agent launch.
|
|
3629
|
+
`
|
|
3630
|
+
);
|
|
3631
|
+
}
|
|
3632
|
+
try {
|
|
3633
|
+
const ctxDir = path5.join(os4.homedir(), ".exe-os", "session-cache");
|
|
3634
|
+
mkdirSync2(ctxDir, { recursive: true });
|
|
3635
|
+
const ctxFile = path5.join(ctxDir, `session-context-${sessionName}.md`);
|
|
3636
|
+
const ctxContent = [
|
|
3637
|
+
`## Session Context`,
|
|
3638
|
+
`You are running in tmux session: ${sessionName}.`,
|
|
3639
|
+
`Your parent coordinator session is ${exeSession}.`,
|
|
3640
|
+
`Your employees (if any) use the -${exeSession} suffix.`
|
|
3641
|
+
].join("\n");
|
|
3642
|
+
atomicWriteSync(ctxFile, ctxContent);
|
|
3643
|
+
void ctxFile;
|
|
3644
|
+
} catch {
|
|
3645
|
+
}
|
|
3646
|
+
let envPrefix = `AGENT_ID=${employeeName} EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName} EXE_SESSION_START_ISO=${(/* @__PURE__ */ new Date()).toISOString()} EXE_OS_ENABLE_LEAN_MCP=1`;
|
|
3647
|
+
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
3648
|
+
const cfg = PROVIDER_TABLE[ccProvider];
|
|
3649
|
+
if (cfg?.apiKeyEnv) {
|
|
3650
|
+
const keyVal = process.env[cfg.apiKeyEnv];
|
|
3651
|
+
if (keyVal) {
|
|
3652
|
+
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
if (useCodex) {
|
|
3657
|
+
const codexCfg = RUNTIME_TABLE.codex;
|
|
3658
|
+
if (codexCfg?.apiKeyEnv) {
|
|
3659
|
+
const keyVal = process.env[codexCfg.apiKeyEnv];
|
|
3660
|
+
if (keyVal) {
|
|
3661
|
+
envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
|
|
3665
|
+
if (agentRtConfig.reasoning_effort) {
|
|
3666
|
+
envPrefix = `${envPrefix} EXE_AGENT_REASONING_EFFORT=${agentRtConfig.reasoning_effort}`;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
if (useOpencode) {
|
|
3670
|
+
const ocCfg = PROVIDER_TABLE.opencode;
|
|
3671
|
+
if (ocCfg?.apiKeyEnv) {
|
|
3672
|
+
const keyVal = process.env[ocCfg.apiKeyEnv];
|
|
3673
|
+
if (keyVal) {
|
|
3674
|
+
envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
|
|
3678
|
+
}
|
|
3679
|
+
if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
|
|
3680
|
+
if (agentRtConfig.runtime === "claude" && agentRtConfig.model) {
|
|
3681
|
+
const ccModel = normalizeCcModelName(agentRtConfig.model);
|
|
3682
|
+
envPrefix = `${envPrefix} ANTHROPIC_MODEL=${ccModel}`;
|
|
3683
|
+
if (opts?.modelOverride) {
|
|
3684
|
+
envPrefix = `${envPrefix} EXE_AGENT_MODEL=${ccModel}`;
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
let spawnCommand;
|
|
3689
|
+
if (useExeAgent) {
|
|
3690
|
+
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
3691
|
+
} else if (useCodex) {
|
|
3692
|
+
process.stderr.write(
|
|
3693
|
+
`[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
|
|
3694
|
+
`
|
|
3695
|
+
);
|
|
3696
|
+
spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName} --session ${sessionName}${cleanupSuffix}`;
|
|
3697
|
+
} else if (useOpencode) {
|
|
3698
|
+
const binName = `${employeeName}-opencode`;
|
|
3699
|
+
process.stderr.write(
|
|
3700
|
+
`[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
|
|
3701
|
+
`
|
|
3702
|
+
);
|
|
3703
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
3704
|
+
} else if (useBinSymlink) {
|
|
3705
|
+
const binName = `${employeeName}-${ccProvider}`;
|
|
3706
|
+
process.stderr.write(
|
|
3707
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
3708
|
+
`
|
|
3709
|
+
);
|
|
3710
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
3711
|
+
} else {
|
|
3712
|
+
spawnCommand = `${envPrefix} exe-launch-agent --agent ${employeeName}${cleanupSuffix}`;
|
|
3713
|
+
}
|
|
3714
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
3715
|
+
cwd: spawnCwd,
|
|
3716
|
+
command: spawnCommand
|
|
3717
|
+
});
|
|
3718
|
+
if (spawnResult.error) {
|
|
3719
|
+
releaseSpawnLock(sessionName);
|
|
3720
|
+
recordOrchestrationEventBestEffort({
|
|
3721
|
+
eventType: "tmux.spawn.completed",
|
|
3722
|
+
source: "tmux-routing.spawnEmployee",
|
|
3723
|
+
severity: "warn",
|
|
3724
|
+
agentId: employeeName,
|
|
3725
|
+
sessionScope: exeSession,
|
|
3726
|
+
instanceId: opts?.instance != null ? String(opts.instance) : null,
|
|
3727
|
+
tmuxSession: sessionName,
|
|
3728
|
+
runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
|
|
3729
|
+
durationMs: Date.now() - spawnStartedAt,
|
|
3730
|
+
result: "failed",
|
|
3731
|
+
errorCode: "tmux_new_session_failed"
|
|
3732
|
+
});
|
|
3733
|
+
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
3734
|
+
}
|
|
3735
|
+
transport.pipeLog(sessionName, logFile);
|
|
3736
|
+
try {
|
|
3737
|
+
const mySession = getMySession();
|
|
3738
|
+
const dispatchInfo = path5.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
3739
|
+
atomicWriteJsonSync(dispatchInfo, {
|
|
3740
|
+
dispatchedBy: mySession,
|
|
3741
|
+
rootExe: exeSession,
|
|
3742
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
|
|
3743
|
+
runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
|
|
3744
|
+
model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
|
|
3745
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3746
|
+
});
|
|
3747
|
+
} catch {
|
|
3748
|
+
}
|
|
3749
|
+
const callerIsDaemon = process.env.EXE_IS_DAEMON === "1";
|
|
3750
|
+
const POLL_CAP_MS = callerIsDaemon ? 0 : 45e3;
|
|
3751
|
+
const POLL_INTERVAL_MS = 500;
|
|
3752
|
+
const pollIterations = Math.ceil(POLL_CAP_MS / POLL_INTERVAL_MS);
|
|
3753
|
+
let booted = false;
|
|
3754
|
+
for (let i = 0; i < pollIterations; i++) {
|
|
3755
|
+
try {
|
|
3756
|
+
execSync3("sleep 0.5");
|
|
3757
|
+
} catch {
|
|
3758
|
+
}
|
|
3759
|
+
try {
|
|
3760
|
+
const pane = transport.capturePane(sessionName);
|
|
3761
|
+
if (useExeAgent) {
|
|
3762
|
+
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
3763
|
+
booted = true;
|
|
3764
|
+
break;
|
|
3765
|
+
}
|
|
3766
|
+
} else if (useCodex) {
|
|
3767
|
+
if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
|
|
3768
|
+
booted = true;
|
|
3769
|
+
break;
|
|
3770
|
+
}
|
|
3771
|
+
} else {
|
|
3772
|
+
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
3773
|
+
booted = true;
|
|
3774
|
+
break;
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
} catch {
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
if (!booted) {
|
|
3781
|
+
releaseSpawnLock(sessionName);
|
|
3782
|
+
const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
|
|
3783
|
+
process.stderr.write(
|
|
3784
|
+
`[tmux-routing] ${runtimeLabel} boot poll capped at ${POLL_CAP_MS / 1e3}s for ${sessionName} \u2014 sending task nudge anyway
|
|
3785
|
+
`
|
|
3786
|
+
);
|
|
3787
|
+
}
|
|
3788
|
+
if (!useExeAgent && !useCodex) {
|
|
3789
|
+
if (callerIsDaemon) {
|
|
3790
|
+
try {
|
|
3791
|
+
const empRoster = loadEmployeesSync();
|
|
3792
|
+
const bootCache = {
|
|
3793
|
+
agentId: employeeName,
|
|
3794
|
+
agentRole: empRoster.find((e) => baseAgentName(e.name) === baseAgentName(employeeName))?.role ?? "employee",
|
|
3795
|
+
sessionName,
|
|
3796
|
+
exeSession,
|
|
3797
|
+
dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3798
|
+
};
|
|
3799
|
+
const cachePath = path5.join(os4.homedir(), ".exe-os", "session-cache", `boot-cache-${sessionName}.json`);
|
|
3800
|
+
writeFileSync(cachePath, JSON.stringify(bootCache));
|
|
3801
|
+
} catch {
|
|
3802
|
+
}
|
|
3803
|
+
const _transport = transport;
|
|
3804
|
+
const _session = sessionName;
|
|
3805
|
+
const _emp = employeeName;
|
|
3806
|
+
setTimeout(() => {
|
|
3807
|
+
try {
|
|
3808
|
+
_transport.sendKeysLiteral(_session, `You have pending tasks. Run list_tasks and start your highest priority task immediately.`);
|
|
3809
|
+
} catch (e) {
|
|
3810
|
+
process.stderr.write(`[tmux-routing] deferred nudge ${_emp}: ${e.message}
|
|
3811
|
+
`);
|
|
3812
|
+
}
|
|
3813
|
+
}, 1e4);
|
|
3814
|
+
} else {
|
|
3815
|
+
try {
|
|
3816
|
+
transport.sendKeysLiteral(sessionName, `You have pending tasks. Run list_tasks and start immediately.`);
|
|
3817
|
+
} catch (e) {
|
|
3818
|
+
process.stderr.write(`[tmux-routing] CLI nudge ${employeeName}: ${e instanceof Error ? e.message : String(e)}
|
|
3819
|
+
`);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
registerSession({
|
|
3824
|
+
windowName: sessionName,
|
|
3825
|
+
agentId: employeeName,
|
|
3826
|
+
projectDir: spawnCwd,
|
|
3827
|
+
parentExe: exeSession,
|
|
3828
|
+
pid: 0,
|
|
3829
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3830
|
+
});
|
|
3831
|
+
releaseSpawnLock(sessionName);
|
|
3832
|
+
recordOrchestrationEventBestEffort({
|
|
3833
|
+
eventType: "tmux.spawn.completed",
|
|
3834
|
+
source: "tmux-routing.spawnEmployee",
|
|
3835
|
+
agentId: employeeName,
|
|
3836
|
+
sessionScope: exeSession,
|
|
3837
|
+
instanceId: opts?.instance != null ? String(opts.instance) : null,
|
|
3838
|
+
tmuxSession: sessionName,
|
|
3839
|
+
runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
|
|
3840
|
+
durationMs: Date.now() - spawnStartedAt,
|
|
3841
|
+
result: booted ? "spawned_booted" : "spawned_unverified"
|
|
3842
|
+
});
|
|
3843
|
+
return { sessionName };
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
// src/lib/task-scope.ts
|
|
3847
|
+
function getCurrentSessionScope() {
|
|
3848
|
+
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
|
3849
|
+
return null;
|
|
3850
|
+
}
|
|
3851
|
+
try {
|
|
3852
|
+
const scope = resolveExeSession();
|
|
3853
|
+
if (!scope && process.env.NODE_ENV !== "test") {
|
|
3854
|
+
if (!_scopeWarningEmitted) {
|
|
3855
|
+
_scopeWarningEmitted = true;
|
|
3856
|
+
process.stderr.write(
|
|
3857
|
+
"[task-scope] WARN: session scope resolution returned null in production. All session-scoped queries will run UNSCOPED until a session is resolved. This is expected in non-tmux environments but a bug if running in tmux.\n"
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
return scope;
|
|
3862
|
+
} catch {
|
|
3863
|
+
return null;
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
var _scopeWarningEmitted = false;
|
|
3867
|
+
function sessionScopeFilter(sessionScope, tableAlias) {
|
|
3868
|
+
const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
|
|
3869
|
+
if (!scope) return { sql: "", args: [] };
|
|
3870
|
+
const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
|
|
3871
|
+
return {
|
|
3872
|
+
sql: ` AND (${col} IS NULL OR ${col} = ?)`,
|
|
3873
|
+
args: [scope]
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3876
|
+
function strictSessionScopeFilter(sessionScope, tableAlias) {
|
|
3877
|
+
const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
|
|
3878
|
+
if (!scope) return { sql: "", args: [] };
|
|
3879
|
+
const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
|
|
3880
|
+
return {
|
|
3881
|
+
sql: ` AND ${col} = ?`,
|
|
3882
|
+
args: [scope]
|
|
3883
|
+
};
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
export {
|
|
3887
|
+
_resetCcAgentSupportCache,
|
|
3888
|
+
claudeSupportsAgentFlag,
|
|
3889
|
+
PROVIDER_TABLE,
|
|
3890
|
+
DEFAULT_PROVIDER,
|
|
3891
|
+
logTaskDispatch,
|
|
3892
|
+
getCurrentSessionScope,
|
|
3893
|
+
sessionScopeFilter,
|
|
3894
|
+
strictSessionScopeFilter,
|
|
3895
|
+
writeNotification,
|
|
3896
|
+
readUnreadNotifications,
|
|
3897
|
+
markAsRead,
|
|
3898
|
+
markAsReadByTaskFile,
|
|
3899
|
+
cleanupOldNotifications,
|
|
3900
|
+
markDoneTaskNotificationsAsRead,
|
|
3901
|
+
formatNotifications,
|
|
3902
|
+
migrateJsonNotifications,
|
|
3903
|
+
isStructuredTaskCompletionReport,
|
|
3904
|
+
buildTaskCompletionReport,
|
|
3905
|
+
writeCheckpoint,
|
|
3906
|
+
extractParentFromContext,
|
|
3907
|
+
slugify,
|
|
3908
|
+
resolveTask,
|
|
3909
|
+
createTaskCore,
|
|
3910
|
+
queryTaskRows,
|
|
3911
|
+
listTasks,
|
|
3912
|
+
isTmuxSessionAlive,
|
|
3913
|
+
TASK_ALREADY_CLAIMED_PREFIX,
|
|
3914
|
+
checkStaleCompletion,
|
|
3915
|
+
updateTaskStatus,
|
|
3916
|
+
deleteTaskCore,
|
|
3917
|
+
ensureArchitectureDoc,
|
|
3918
|
+
ensureGitignoreExe,
|
|
3919
|
+
cleanOrphanedTaskFiles,
|
|
3920
|
+
readLatestCheckpoint,
|
|
3921
|
+
createTaskGroup,
|
|
3922
|
+
getGroupStatus,
|
|
3923
|
+
getGroupResults,
|
|
3924
|
+
getAggregatedGroupResults,
|
|
3925
|
+
checkAndFireBarriers,
|
|
3926
|
+
checkTaskFileConsistency,
|
|
3927
|
+
formatAge,
|
|
3928
|
+
isStale,
|
|
3929
|
+
countPendingReviews,
|
|
3930
|
+
countNewPendingReviewsSince,
|
|
3931
|
+
listPendingReviews,
|
|
3932
|
+
cleanupOrphanedReviews,
|
|
3933
|
+
getReviewChecklist,
|
|
3934
|
+
createReviewForCompletedTask,
|
|
3935
|
+
cleanupReviewFile,
|
|
3936
|
+
formatIntercomPrompt,
|
|
3937
|
+
acquireSpawnLock,
|
|
3938
|
+
releaseSpawnLock,
|
|
3939
|
+
getMySession,
|
|
3940
|
+
employeeSessionName,
|
|
3941
|
+
parseParentExe,
|
|
3942
|
+
extractRootExe,
|
|
3943
|
+
registerParentExe,
|
|
3944
|
+
getParentExe,
|
|
3945
|
+
getDispatchedBy,
|
|
3946
|
+
resolveExeSession,
|
|
3947
|
+
isEmployeeAlive,
|
|
3948
|
+
findFreeInstance,
|
|
3949
|
+
verifyPaneAtCapacity,
|
|
3950
|
+
getSessionState,
|
|
3951
|
+
isSessionBusy,
|
|
3952
|
+
isExeSession,
|
|
3953
|
+
sendIntercom,
|
|
3954
|
+
notifyParentExe,
|
|
3955
|
+
notifyCoordinatorTaskCompletion,
|
|
3956
|
+
ensureEmployee,
|
|
3957
|
+
spawnEmployee
|
|
3958
|
+
};
|