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