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