@askexenow/exe-os 0.8.83 → 0.8.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/bin/backfill-conversations.js +746 -595
  2. package/dist/bin/backfill-responses.js +745 -594
  3. package/dist/bin/backfill-vectors.js +312 -226
  4. package/dist/bin/cleanup-stale-review-tasks.js +97 -2
  5. package/dist/bin/cli.js +14350 -12518
  6. package/dist/bin/exe-agent.js +97 -88
  7. package/dist/bin/exe-assign.js +1003 -854
  8. package/dist/bin/exe-boot.js +1257 -320
  9. package/dist/bin/exe-call.js +10 -0
  10. package/dist/bin/exe-cloud.js +29 -6
  11. package/dist/bin/exe-dispatch.js +210 -34
  12. package/dist/bin/exe-doctor.js +403 -6
  13. package/dist/bin/exe-export-behaviors.js +175 -72
  14. package/dist/bin/exe-forget.js +97 -2
  15. package/dist/bin/exe-gateway.js +550 -171
  16. package/dist/bin/exe-healthcheck.js +1 -0
  17. package/dist/bin/exe-heartbeat.js +100 -5
  18. package/dist/bin/exe-kill.js +175 -72
  19. package/dist/bin/exe-launch-agent.js +189 -76
  20. package/dist/bin/exe-link.js +902 -80
  21. package/dist/bin/exe-new-employee.js +38 -8
  22. package/dist/bin/exe-pending-messages.js +96 -2
  23. package/dist/bin/exe-pending-notifications.js +97 -2
  24. package/dist/bin/exe-pending-reviews.js +98 -3
  25. package/dist/bin/exe-rename.js +564 -23
  26. package/dist/bin/exe-review.js +231 -73
  27. package/dist/bin/exe-search.js +989 -226
  28. package/dist/bin/exe-session-cleanup.js +4806 -1665
  29. package/dist/bin/exe-settings.js +20 -5
  30. package/dist/bin/exe-status.js +97 -2
  31. package/dist/bin/exe-team.js +97 -2
  32. package/dist/bin/git-sweep.js +899 -207
  33. package/dist/bin/graph-backfill.js +175 -72
  34. package/dist/bin/graph-export.js +175 -72
  35. package/dist/bin/install.js +38 -7
  36. package/dist/bin/list-providers.js +1 -0
  37. package/dist/bin/scan-tasks.js +904 -211
  38. package/dist/bin/setup.js +867 -268
  39. package/dist/bin/shard-migrate.js +175 -72
  40. package/dist/bin/update.js +1 -0
  41. package/dist/bin/wiki-sync.js +175 -72
  42. package/dist/gateway/index.js +548 -166
  43. package/dist/hooks/bug-report-worker.js +208 -23
  44. package/dist/hooks/commit-complete.js +897 -205
  45. package/dist/hooks/error-recall.js +988 -226
  46. package/dist/hooks/ingest-worker.js +1638 -1194
  47. package/dist/hooks/ingest.js +3 -0
  48. package/dist/hooks/instructions-loaded.js +707 -97
  49. package/dist/hooks/notification.js +699 -89
  50. package/dist/hooks/post-compact.js +714 -104
  51. package/dist/hooks/pre-compact.js +897 -205
  52. package/dist/hooks/pre-tool-use.js +742 -123
  53. package/dist/hooks/prompt-ingest-worker.js +242 -101
  54. package/dist/hooks/prompt-submit.js +995 -233
  55. package/dist/hooks/response-ingest-worker.js +242 -101
  56. package/dist/hooks/session-end.js +3941 -400
  57. package/dist/hooks/session-start.js +1001 -226
  58. package/dist/hooks/stop.js +725 -115
  59. package/dist/hooks/subagent-stop.js +714 -104
  60. package/dist/hooks/summary-worker.js +1964 -1330
  61. package/dist/index.js +1651 -1053
  62. package/dist/lib/cloud-sync.js +907 -86
  63. package/dist/lib/consolidation.js +2 -1
  64. package/dist/lib/database.js +642 -87
  65. package/dist/lib/db-daemon-client.js +503 -0
  66. package/dist/lib/device-registry.js +547 -7
  67. package/dist/lib/embedder.js +14 -28
  68. package/dist/lib/employee-templates.js +84 -74
  69. package/dist/lib/employees.js +9 -0
  70. package/dist/lib/exe-daemon-client.js +16 -29
  71. package/dist/lib/exe-daemon.js +1955 -922
  72. package/dist/lib/hybrid-search.js +988 -226
  73. package/dist/lib/identity.js +87 -67
  74. package/dist/lib/keychain.js +9 -1
  75. package/dist/lib/messaging.js +8 -1
  76. package/dist/lib/reminders.js +91 -74
  77. package/dist/lib/schedules.js +96 -2
  78. package/dist/lib/skill-learning.js +103 -85
  79. package/dist/lib/store.js +234 -73
  80. package/dist/lib/tasks.js +111 -22
  81. package/dist/lib/tmux-routing.js +120 -31
  82. package/dist/lib/token-spend.js +273 -0
  83. package/dist/lib/ws-client.js +11 -0
  84. package/dist/mcp/server.js +5222 -475
  85. package/dist/mcp/tools/complete-reminder.js +94 -77
  86. package/dist/mcp/tools/create-reminder.js +94 -77
  87. package/dist/mcp/tools/create-task.js +120 -22
  88. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  89. package/dist/mcp/tools/list-reminders.js +94 -77
  90. package/dist/mcp/tools/list-tasks.js +31 -1
  91. package/dist/mcp/tools/send-message.js +8 -1
  92. package/dist/mcp/tools/update-task.js +39 -10
  93. package/dist/runtime/index.js +911 -219
  94. package/dist/tui/App.js +997 -295
  95. package/package.json +6 -1
@@ -607,6 +607,443 @@ var init_employees = __esm({
607
607
  }
608
608
  });
609
609
 
610
+ // src/lib/exe-daemon-client.ts
611
+ import net from "net";
612
+ import { spawn } from "child_process";
613
+ import { randomUUID } from "crypto";
614
+ import { existsSync as existsSync5, unlinkSync as unlinkSync2, readFileSync as readFileSync5, openSync, closeSync, statSync } from "fs";
615
+ import path5 from "path";
616
+ import { fileURLToPath } from "url";
617
+ function handleData(chunk) {
618
+ _buffer += chunk.toString();
619
+ if (_buffer.length > MAX_BUFFER) {
620
+ _buffer = "";
621
+ return;
622
+ }
623
+ let newlineIdx;
624
+ while ((newlineIdx = _buffer.indexOf("\n")) !== -1) {
625
+ const line = _buffer.slice(0, newlineIdx).trim();
626
+ _buffer = _buffer.slice(newlineIdx + 1);
627
+ if (!line) continue;
628
+ try {
629
+ const response = JSON.parse(line);
630
+ const id = response.id;
631
+ if (!id) continue;
632
+ const entry = _pending.get(id);
633
+ if (entry) {
634
+ clearTimeout(entry.timer);
635
+ _pending.delete(id);
636
+ entry.resolve(response);
637
+ }
638
+ } catch {
639
+ }
640
+ }
641
+ }
642
+ function cleanupStaleFiles() {
643
+ if (existsSync5(PID_PATH)) {
644
+ try {
645
+ const pid = parseInt(readFileSync5(PID_PATH, "utf8").trim(), 10);
646
+ if (pid > 0) {
647
+ try {
648
+ process.kill(pid, 0);
649
+ return;
650
+ } catch {
651
+ }
652
+ }
653
+ } catch {
654
+ }
655
+ try {
656
+ unlinkSync2(PID_PATH);
657
+ } catch {
658
+ }
659
+ try {
660
+ unlinkSync2(SOCKET_PATH);
661
+ } catch {
662
+ }
663
+ }
664
+ }
665
+ function findPackageRoot() {
666
+ let dir = path5.dirname(fileURLToPath(import.meta.url));
667
+ const { root } = path5.parse(dir);
668
+ while (dir !== root) {
669
+ if (existsSync5(path5.join(dir, "package.json"))) return dir;
670
+ dir = path5.dirname(dir);
671
+ }
672
+ return null;
673
+ }
674
+ function spawnDaemon() {
675
+ const pkgRoot = findPackageRoot();
676
+ if (!pkgRoot) {
677
+ process.stderr.write("[exed-client] WARN: cannot find package root\n");
678
+ return;
679
+ }
680
+ const daemonPath = path5.join(pkgRoot, "dist", "lib", "exe-daemon.js");
681
+ if (!existsSync5(daemonPath)) {
682
+ process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
683
+ `);
684
+ return;
685
+ }
686
+ const resolvedPath = daemonPath;
687
+ process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
688
+ `);
689
+ const logPath = path5.join(path5.dirname(SOCKET_PATH), "exed.log");
690
+ let stderrFd = "ignore";
691
+ try {
692
+ stderrFd = openSync(logPath, "a");
693
+ } catch {
694
+ }
695
+ const child = spawn(process.execPath, [resolvedPath], {
696
+ detached: true,
697
+ stdio: ["ignore", "ignore", stderrFd],
698
+ env: {
699
+ ...process.env,
700
+ TMUX: void 0,
701
+ // Daemon is global — must not inherit session scope
702
+ TMUX_PANE: void 0,
703
+ // Prevents resolveExeSession() from scoping to one session
704
+ EXE_DAEMON_SOCK: SOCKET_PATH,
705
+ EXE_DAEMON_PID: PID_PATH
706
+ }
707
+ });
708
+ child.unref();
709
+ if (typeof stderrFd === "number") {
710
+ try {
711
+ closeSync(stderrFd);
712
+ } catch {
713
+ }
714
+ }
715
+ }
716
+ function acquireSpawnLock() {
717
+ try {
718
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
719
+ closeSync(fd);
720
+ return true;
721
+ } catch {
722
+ try {
723
+ const stat = statSync(SPAWN_LOCK_PATH);
724
+ if (Date.now() - stat.mtimeMs > SPAWN_LOCK_STALE_MS) {
725
+ try {
726
+ unlinkSync2(SPAWN_LOCK_PATH);
727
+ } catch {
728
+ }
729
+ try {
730
+ const fd = openSync(SPAWN_LOCK_PATH, "wx");
731
+ closeSync(fd);
732
+ return true;
733
+ } catch {
734
+ }
735
+ }
736
+ } catch {
737
+ }
738
+ return false;
739
+ }
740
+ }
741
+ function releaseSpawnLock() {
742
+ try {
743
+ unlinkSync2(SPAWN_LOCK_PATH);
744
+ } catch {
745
+ }
746
+ }
747
+ function connectToSocket() {
748
+ return new Promise((resolve) => {
749
+ if (_socket && _connected) {
750
+ resolve(true);
751
+ return;
752
+ }
753
+ const socket = net.createConnection({ path: SOCKET_PATH });
754
+ const connectTimeout = setTimeout(() => {
755
+ socket.destroy();
756
+ resolve(false);
757
+ }, 2e3);
758
+ socket.on("connect", () => {
759
+ clearTimeout(connectTimeout);
760
+ _socket = socket;
761
+ _connected = true;
762
+ _buffer = "";
763
+ socket.on("data", handleData);
764
+ socket.on("close", () => {
765
+ _connected = false;
766
+ _socket = null;
767
+ for (const [id, entry] of _pending) {
768
+ clearTimeout(entry.timer);
769
+ _pending.delete(id);
770
+ entry.resolve({ error: "Connection closed" });
771
+ }
772
+ });
773
+ socket.on("error", () => {
774
+ _connected = false;
775
+ _socket = null;
776
+ });
777
+ resolve(true);
778
+ });
779
+ socket.on("error", () => {
780
+ clearTimeout(connectTimeout);
781
+ resolve(false);
782
+ });
783
+ });
784
+ }
785
+ async function connectEmbedDaemon() {
786
+ if (_socket && _connected) return true;
787
+ if (await connectToSocket()) return true;
788
+ if (acquireSpawnLock()) {
789
+ try {
790
+ cleanupStaleFiles();
791
+ spawnDaemon();
792
+ } finally {
793
+ releaseSpawnLock();
794
+ }
795
+ }
796
+ const start = Date.now();
797
+ let delay2 = 100;
798
+ while (Date.now() - start < CONNECT_TIMEOUT_MS) {
799
+ await new Promise((r) => setTimeout(r, delay2));
800
+ if (await connectToSocket()) return true;
801
+ delay2 = Math.min(delay2 * 2, 3e3);
802
+ }
803
+ return false;
804
+ }
805
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
806
+ return new Promise((resolve) => {
807
+ if (!_socket || !_connected) {
808
+ resolve({ error: "Not connected" });
809
+ return;
810
+ }
811
+ const id = randomUUID();
812
+ const timer = setTimeout(() => {
813
+ _pending.delete(id);
814
+ resolve({ error: "Request timeout" });
815
+ }, timeoutMs);
816
+ _pending.set(id, { resolve, timer });
817
+ try {
818
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
819
+ } catch {
820
+ clearTimeout(timer);
821
+ _pending.delete(id);
822
+ resolve({ error: "Write failed" });
823
+ }
824
+ });
825
+ }
826
+ function isClientConnected() {
827
+ return _connected;
828
+ }
829
+ var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
830
+ var init_exe_daemon_client = __esm({
831
+ "src/lib/exe-daemon-client.ts"() {
832
+ "use strict";
833
+ init_config();
834
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path5.join(EXE_AI_DIR, "exed.sock");
835
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path5.join(EXE_AI_DIR, "exed.pid");
836
+ SPAWN_LOCK_PATH = path5.join(EXE_AI_DIR, "exed-spawn.lock");
837
+ SPAWN_LOCK_STALE_MS = 3e4;
838
+ CONNECT_TIMEOUT_MS = 15e3;
839
+ REQUEST_TIMEOUT_MS = 3e4;
840
+ _socket = null;
841
+ _connected = false;
842
+ _buffer = "";
843
+ _pending = /* @__PURE__ */ new Map();
844
+ MAX_BUFFER = 1e7;
845
+ }
846
+ });
847
+
848
+ // src/lib/daemon-protocol.ts
849
+ function serializeValue(v) {
850
+ if (v === null || v === void 0) return null;
851
+ if (typeof v === "bigint") return Number(v);
852
+ if (typeof v === "boolean") return v ? 1 : 0;
853
+ if (v instanceof Uint8Array) {
854
+ return { __blob: Buffer.from(v).toString("base64") };
855
+ }
856
+ if (ArrayBuffer.isView(v)) {
857
+ return { __blob: Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString("base64") };
858
+ }
859
+ if (v instanceof ArrayBuffer) {
860
+ return { __blob: Buffer.from(v).toString("base64") };
861
+ }
862
+ if (typeof v === "string" || typeof v === "number") return v;
863
+ return String(v);
864
+ }
865
+ function deserializeValue(v) {
866
+ if (v === null) return null;
867
+ if (typeof v === "object" && v !== null && "__blob" in v) {
868
+ const buf = Buffer.from(v.__blob, "base64");
869
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
870
+ }
871
+ return v;
872
+ }
873
+ function deserializeResultSet(srs) {
874
+ const rows = srs.rows.map((obj) => {
875
+ const values = srs.columns.map(
876
+ (col) => deserializeValue(obj[col] ?? null)
877
+ );
878
+ const row = values;
879
+ for (let i = 0; i < srs.columns.length; i++) {
880
+ const col = srs.columns[i];
881
+ if (col !== void 0) {
882
+ row[col] = values[i] ?? null;
883
+ }
884
+ }
885
+ Object.defineProperty(row, "length", {
886
+ value: values.length,
887
+ enumerable: false
888
+ });
889
+ return row;
890
+ });
891
+ return {
892
+ columns: srs.columns,
893
+ columnTypes: srs.columnTypes ?? [],
894
+ rows,
895
+ rowsAffected: srs.rowsAffected,
896
+ lastInsertRowid: srs.lastInsertRowid != null ? BigInt(srs.lastInsertRowid) : void 0,
897
+ toJSON: () => ({
898
+ columns: srs.columns,
899
+ columnTypes: srs.columnTypes ?? [],
900
+ rows: srs.rows,
901
+ rowsAffected: srs.rowsAffected,
902
+ lastInsertRowid: srs.lastInsertRowid
903
+ })
904
+ };
905
+ }
906
+ var init_daemon_protocol = __esm({
907
+ "src/lib/daemon-protocol.ts"() {
908
+ "use strict";
909
+ }
910
+ });
911
+
912
+ // src/lib/db-daemon-client.ts
913
+ var db_daemon_client_exports = {};
914
+ __export(db_daemon_client_exports, {
915
+ createDaemonDbClient: () => createDaemonDbClient,
916
+ initDaemonDbClient: () => initDaemonDbClient
917
+ });
918
+ function normalizeStatement(stmt) {
919
+ if (typeof stmt === "string") {
920
+ return { sql: stmt, args: [] };
921
+ }
922
+ const sql = stmt.sql;
923
+ let args2 = [];
924
+ if (Array.isArray(stmt.args)) {
925
+ args2 = stmt.args.map((v) => serializeValue(v));
926
+ } else if (stmt.args && typeof stmt.args === "object") {
927
+ const named = {};
928
+ for (const [key, val] of Object.entries(stmt.args)) {
929
+ named[key] = serializeValue(val);
930
+ }
931
+ return { sql, args: named };
932
+ }
933
+ return { sql, args: args2 };
934
+ }
935
+ function createDaemonDbClient(fallbackClient) {
936
+ let _useDaemon = false;
937
+ const client = {
938
+ async execute(stmt) {
939
+ if (!_useDaemon || !isClientConnected()) {
940
+ return fallbackClient.execute(stmt);
941
+ }
942
+ const { sql, args: args2 } = normalizeStatement(stmt);
943
+ const response = await sendDaemonRequest({
944
+ type: "db-execute",
945
+ sql,
946
+ args: args2
947
+ });
948
+ if (response.error) {
949
+ const errMsg = String(response.error);
950
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
951
+ process.stderr.write(`[db-daemon] Transport error (${errMsg}), falling back to direct
952
+ `);
953
+ return fallbackClient.execute(stmt);
954
+ }
955
+ throw new Error(errMsg);
956
+ }
957
+ if (response.db) {
958
+ return deserializeResultSet(response.db);
959
+ }
960
+ process.stderr.write("[db-daemon] Unexpected response shape, falling back to direct\n");
961
+ return fallbackClient.execute(stmt);
962
+ },
963
+ async batch(stmts, mode) {
964
+ if (!_useDaemon || !isClientConnected()) {
965
+ return fallbackClient.batch(stmts, mode);
966
+ }
967
+ const statements = stmts.map(normalizeStatement);
968
+ const response = await sendDaemonRequest({
969
+ type: "db-batch",
970
+ statements,
971
+ mode: mode ?? "deferred"
972
+ });
973
+ if (response.error) {
974
+ const errMsg = String(response.error);
975
+ if (errMsg === "Not connected" || errMsg === "Request timeout" || errMsg === "Write failed") {
976
+ process.stderr.write(`[db-daemon] Batch transport error (${errMsg}), falling back to direct
977
+ `);
978
+ return fallbackClient.batch(stmts, mode);
979
+ }
980
+ throw new Error(errMsg);
981
+ }
982
+ const batchResults = response["db-batch"];
983
+ if (batchResults) {
984
+ return batchResults.map(deserializeResultSet);
985
+ }
986
+ process.stderr.write("[db-daemon] Unexpected batch response shape, falling back to direct\n");
987
+ return fallbackClient.batch(stmts, mode);
988
+ },
989
+ // Transaction support — delegate to fallback (transactions need direct connection)
990
+ async transaction(mode) {
991
+ return fallbackClient.transaction(mode);
992
+ },
993
+ // executeMultiple — delegate to fallback (used only for schema migrations)
994
+ async executeMultiple(sql) {
995
+ return fallbackClient.executeMultiple(sql);
996
+ },
997
+ // migrate — delegate to fallback
998
+ async migrate(stmts) {
999
+ return fallbackClient.migrate(stmts);
1000
+ },
1001
+ // Sync mode — delegate to fallback
1002
+ sync() {
1003
+ return fallbackClient.sync();
1004
+ },
1005
+ close() {
1006
+ _useDaemon = false;
1007
+ },
1008
+ get closed() {
1009
+ return fallbackClient.closed;
1010
+ },
1011
+ get protocol() {
1012
+ return fallbackClient.protocol;
1013
+ }
1014
+ };
1015
+ return {
1016
+ ...client,
1017
+ /** Enable daemon routing (call after confirming daemon is connected) */
1018
+ _enableDaemon() {
1019
+ _useDaemon = true;
1020
+ },
1021
+ /** Check if daemon routing is active */
1022
+ _isDaemonActive() {
1023
+ return _useDaemon && isClientConnected();
1024
+ }
1025
+ };
1026
+ }
1027
+ async function initDaemonDbClient(fallbackClient) {
1028
+ if (process.env.EXE_IS_DAEMON === "1") return null;
1029
+ const connected = await connectEmbedDaemon();
1030
+ if (!connected) {
1031
+ process.stderr.write("[db-daemon] Daemon unavailable \u2014 using direct SQLite\n");
1032
+ return null;
1033
+ }
1034
+ const client = createDaemonDbClient(fallbackClient);
1035
+ client._enableDaemon();
1036
+ process.stderr.write("[db-daemon] DB routing through daemon (single-writer)\n");
1037
+ return client;
1038
+ }
1039
+ var init_db_daemon_client = __esm({
1040
+ "src/lib/db-daemon-client.ts"() {
1041
+ "use strict";
1042
+ init_exe_daemon_client();
1043
+ init_daemon_protocol();
1044
+ }
1045
+ });
1046
+
610
1047
  // src/lib/database.ts
611
1048
  var database_exports = {};
612
1049
  __export(database_exports, {
@@ -615,6 +1052,7 @@ __export(database_exports, {
615
1052
  ensureSchema: () => ensureSchema,
616
1053
  getClient: () => getClient,
617
1054
  getRawClient: () => getRawClient,
1055
+ initDaemonClient: () => initDaemonClient,
618
1056
  initDatabase: () => initDatabase,
619
1057
  initTurso: () => initTurso,
620
1058
  isInitialized: () => isInitialized
@@ -642,8 +1080,27 @@ function getClient() {
642
1080
  if (!_resilientClient) {
643
1081
  throw new Error("Database client not initialized. Call initDatabase() first.");
644
1082
  }
1083
+ if (process.env.EXE_IS_DAEMON === "1") {
1084
+ return _resilientClient;
1085
+ }
1086
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
1087
+ return _daemonClient;
1088
+ }
645
1089
  return _resilientClient;
646
1090
  }
1091
+ async function initDaemonClient() {
1092
+ if (process.env.EXE_IS_DAEMON === "1") return;
1093
+ if (!_resilientClient) return;
1094
+ try {
1095
+ const { initDaemonDbClient: initDaemonDbClient2 } = await Promise.resolve().then(() => (init_db_daemon_client(), db_daemon_client_exports));
1096
+ _daemonClient = await initDaemonDbClient2(_resilientClient);
1097
+ } catch (err) {
1098
+ process.stderr.write(
1099
+ `[database] Daemon client init failed (non-fatal): ${err instanceof Error ? err.message : String(err)}
1100
+ `
1101
+ );
1102
+ }
1103
+ }
647
1104
  function getRawClient() {
648
1105
  if (!_client) {
649
1106
  throw new Error("Database client not initialized. Call initDatabase() first.");
@@ -1130,6 +1587,12 @@ async function ensureSchema() {
1130
1587
  } catch {
1131
1588
  }
1132
1589
  }
1590
+ try {
1591
+ await client.execute(
1592
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1593
+ );
1594
+ } catch {
1595
+ }
1133
1596
  await client.executeMultiple(`
1134
1597
  CREATE TABLE IF NOT EXISTS entities (
1135
1598
  id TEXT PRIMARY KEY,
@@ -1182,7 +1645,30 @@ async function ensureSchema() {
1182
1645
  entity_id TEXT NOT NULL,
1183
1646
  PRIMARY KEY (hyperedge_id, entity_id)
1184
1647
  );
1648
+
1649
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1650
+ name,
1651
+ content=entities,
1652
+ content_rowid=rowid
1653
+ );
1654
+
1655
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1656
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1657
+ END;
1658
+
1659
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1660
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1661
+ END;
1662
+
1663
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1664
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1665
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1666
+ END;
1185
1667
  `);
1668
+ try {
1669
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1670
+ } catch {
1671
+ }
1186
1672
  await client.executeMultiple(`
1187
1673
  CREATE TABLE IF NOT EXISTS entity_aliases (
1188
1674
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1363,6 +1849,33 @@ async function ensureSchema() {
1363
1849
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1364
1850
  ON conversations(channel_id);
1365
1851
  `);
1852
+ await client.executeMultiple(`
1853
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1854
+ session_uuid TEXT PRIMARY KEY,
1855
+ agent_id TEXT NOT NULL,
1856
+ session_name TEXT,
1857
+ task_id TEXT,
1858
+ project_name TEXT,
1859
+ started_at TEXT NOT NULL
1860
+ );
1861
+
1862
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1863
+ ON session_agent_map(agent_id);
1864
+ `);
1865
+ try {
1866
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1867
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1868
+ await client.execute({
1869
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1870
+ SELECT session_id, agent_id, '', MIN(timestamp)
1871
+ FROM memories
1872
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1873
+ GROUP BY session_id, agent_id`,
1874
+ args: []
1875
+ });
1876
+ }
1877
+ } catch {
1878
+ }
1366
1879
  try {
1367
1880
  await client.execute({
1368
1881
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1496,15 +2009,41 @@ async function ensureSchema() {
1496
2009
  });
1497
2010
  } catch {
1498
2011
  }
2012
+ for (const col of [
2013
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
2014
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
2015
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
2016
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
2017
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
2018
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
2019
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
2020
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
2021
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
2022
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
2023
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
2024
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
2025
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
2026
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
2027
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
2028
+ ]) {
2029
+ try {
2030
+ await client.execute(col);
2031
+ } catch {
2032
+ }
2033
+ }
1499
2034
  }
1500
2035
  async function disposeDatabase() {
2036
+ if (_daemonClient) {
2037
+ _daemonClient.close();
2038
+ _daemonClient = null;
2039
+ }
1501
2040
  if (_client) {
1502
2041
  _client.close();
1503
2042
  _client = null;
1504
2043
  _resilientClient = null;
1505
2044
  }
1506
2045
  }
1507
- var _client, _resilientClient, initTurso, disposeTurso;
2046
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1508
2047
  var init_database = __esm({
1509
2048
  "src/lib/database.ts"() {
1510
2049
  "use strict";
@@ -1512,24 +2051,25 @@ var init_database = __esm({
1512
2051
  init_employees();
1513
2052
  _client = null;
1514
2053
  _resilientClient = null;
2054
+ _daemonClient = null;
1515
2055
  initTurso = initDatabase;
1516
2056
  disposeTurso = disposeDatabase;
1517
2057
  }
1518
2058
  });
1519
2059
 
1520
2060
  // src/lib/license.ts
1521
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
1522
- import { randomUUID } from "crypto";
1523
- import path5 from "path";
2061
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
2062
+ import { randomUUID as randomUUID2 } from "crypto";
2063
+ import path6 from "path";
1524
2064
  import { jwtVerify, importSPKI } from "jose";
1525
2065
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
1526
2066
  var init_license = __esm({
1527
2067
  "src/lib/license.ts"() {
1528
2068
  "use strict";
1529
2069
  init_config();
1530
- LICENSE_PATH = path5.join(EXE_AI_DIR, "license.key");
1531
- CACHE_PATH = path5.join(EXE_AI_DIR, "license-cache.json");
1532
- DEVICE_ID_PATH = path5.join(EXE_AI_DIR, "device-id");
2070
+ LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
2071
+ CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
2072
+ DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
1533
2073
  PLAN_LIMITS = {
1534
2074
  free: { devices: 1, employees: 1, memories: 5e3 },
1535
2075
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -1541,12 +2081,12 @@ var init_license = __esm({
1541
2081
  });
1542
2082
 
1543
2083
  // src/lib/plan-limits.ts
1544
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
1545
- import path6 from "path";
2084
+ import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
2085
+ import path7 from "path";
1546
2086
  function getLicenseSync() {
1547
2087
  try {
1548
- if (!existsSync6(CACHE_PATH2)) return freeLicense();
1549
- const raw = JSON.parse(readFileSync6(CACHE_PATH2, "utf8"));
2088
+ if (!existsSync7(CACHE_PATH2)) return freeLicense();
2089
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
1550
2090
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
1551
2091
  const parts = raw.token.split(".");
1552
2092
  if (parts.length !== 3) return freeLicense();
@@ -1584,8 +2124,8 @@ function assertEmployeeLimitSync(rosterPath) {
1584
2124
  const filePath = rosterPath ?? EMPLOYEES_PATH;
1585
2125
  let count = 0;
1586
2126
  try {
1587
- if (existsSync6(filePath)) {
1588
- const raw = readFileSync6(filePath, "utf8");
2127
+ if (existsSync7(filePath)) {
2128
+ const raw = readFileSync7(filePath, "utf8");
1589
2129
  const employees = JSON.parse(raw);
1590
2130
  count = Array.isArray(employees) ? employees.length : 0;
1591
2131
  }
@@ -1614,19 +2154,19 @@ var init_plan_limits = __esm({
1614
2154
  this.name = "PlanLimitError";
1615
2155
  }
1616
2156
  };
1617
- CACHE_PATH2 = path6.join(EXE_AI_DIR, "license-cache.json");
2157
+ CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
1618
2158
  }
1619
2159
  });
1620
2160
 
1621
2161
  // src/lib/notifications.ts
1622
2162
  import crypto from "crypto";
1623
- import path7 from "path";
2163
+ import path8 from "path";
1624
2164
  import os5 from "os";
1625
2165
  import {
1626
- readFileSync as readFileSync7,
2166
+ readFileSync as readFileSync8,
1627
2167
  readdirSync,
1628
- unlinkSync as unlinkSync2,
1629
- existsSync as existsSync7,
2168
+ unlinkSync as unlinkSync3,
2169
+ existsSync as existsSync8,
1630
2170
  rmdirSync
1631
2171
  } from "fs";
1632
2172
  async function writeNotification(notification) {
@@ -1777,10 +2317,11 @@ __export(tasks_crud_exports, {
1777
2317
  writeCheckpoint: () => writeCheckpoint
1778
2318
  });
1779
2319
  import crypto3 from "crypto";
1780
- import path8 from "path";
2320
+ import path9 from "path";
2321
+ import os6 from "os";
1781
2322
  import { execSync as execSync4 } from "child_process";
1782
2323
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1783
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2324
+ import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1784
2325
  async function writeCheckpoint(input) {
1785
2326
  const client = getClient();
1786
2327
  const row = await resolveTask(client, input.taskId);
@@ -1821,6 +2362,35 @@ function extractParentFromContext(contextBody) {
1821
2362
  function slugify(title) {
1822
2363
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1823
2364
  }
2365
+ function buildKeywordIndex() {
2366
+ const idx = /* @__PURE__ */ new Map();
2367
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
2368
+ for (const kw of keywords) {
2369
+ const existing = idx.get(kw) ?? [];
2370
+ existing.push(role);
2371
+ idx.set(kw, existing);
2372
+ }
2373
+ }
2374
+ return idx;
2375
+ }
2376
+ function checkLaneAffinity(title, context, assigneeName) {
2377
+ const employees = loadEmployeesSync();
2378
+ const employee = employees.find((e) => e.name === assigneeName);
2379
+ if (!employee) return void 0;
2380
+ const assigneeRole = employee.role;
2381
+ const text = `${title} ${context}`.toLowerCase();
2382
+ const matchedRoles = /* @__PURE__ */ new Set();
2383
+ for (const [keyword, roles] of KEYWORD_INDEX) {
2384
+ if (text.includes(keyword)) {
2385
+ for (const role of roles) matchedRoles.add(role);
2386
+ }
2387
+ }
2388
+ if (matchedRoles.size === 0) return void 0;
2389
+ if (matchedRoles.has(assigneeRole)) return void 0;
2390
+ if (assigneeRole === "COO") return void 0;
2391
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
2392
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
2393
+ }
1824
2394
  async function resolveTask(client, identifier, scopeSession) {
1825
2395
  const scope = sessionScopeFilter(scopeSession);
1826
2396
  let result = await client.execute({
@@ -1870,7 +2440,14 @@ async function createTaskCore(input) {
1870
2440
  const id = crypto3.randomUUID();
1871
2441
  const now = (/* @__PURE__ */ new Date()).toISOString();
1872
2442
  const slug = slugify(input.title);
1873
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
2443
+ let earlySessionScope = null;
2444
+ try {
2445
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2446
+ earlySessionScope = resolveExeSession2();
2447
+ } catch {
2448
+ }
2449
+ const scope = earlySessionScope ?? "default";
2450
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
1874
2451
  let blockedById = null;
1875
2452
  const initialStatus = input.blockedBy ? "blocked" : "open";
1876
2453
  if (input.blockedBy) {
@@ -1910,22 +2487,24 @@ async function createTaskCore(input) {
1910
2487
  if (dupCheck.rows.length > 0) {
1911
2488
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
1912
2489
  }
2490
+ if (!process.env.DISABLE_LANE_AFFINITY) {
2491
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
2492
+ if (laneWarning) {
2493
+ warning = warning ? `${warning}
2494
+ ${laneWarning}` : laneWarning;
2495
+ }
2496
+ }
1913
2497
  if (input.baseDir) {
1914
2498
  try {
1915
- await mkdir3(path8.join(input.baseDir, "exe", "output"), { recursive: true });
1916
- await mkdir3(path8.join(input.baseDir, "exe", "research"), { recursive: true });
2499
+ await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
2500
+ await mkdir3(path9.join(input.baseDir, "exe", "research"), { recursive: true });
1917
2501
  await ensureArchitectureDoc(input.baseDir, input.projectName);
1918
2502
  await ensureGitignoreExe(input.baseDir);
1919
2503
  } catch {
1920
2504
  }
1921
2505
  }
1922
2506
  const complexity = input.complexity ?? "standard";
1923
- let sessionScope = null;
1924
- try {
1925
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1926
- sessionScope = resolveExeSession2();
1927
- } catch {
1928
- }
2507
+ const sessionScope = earlySessionScope;
1929
2508
  await client.execute({
1930
2509
  sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, created_at, updated_at)
1931
2510
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -1952,6 +2531,39 @@ async function createTaskCore(input) {
1952
2531
  now
1953
2532
  ]
1954
2533
  });
2534
+ if (input.baseDir) {
2535
+ try {
2536
+ const EXE_OS_DIR = path9.join(os6.homedir(), ".exe-os");
2537
+ const mdPath = path9.join(EXE_OS_DIR, taskFile);
2538
+ const mdDir = path9.dirname(mdPath);
2539
+ if (!existsSync9(mdDir)) await mkdir3(mdDir, { recursive: true });
2540
+ const reviewer = input.reviewer ?? input.assignedBy;
2541
+ const mdContent = `# ${input.title}
2542
+
2543
+ **ID:** ${id}
2544
+ **Status:** ${initialStatus}
2545
+ **Priority:** ${input.priority}
2546
+ **Assigned by:** ${input.assignedBy}
2547
+ **Assigned to:** ${input.assignedTo}
2548
+ **Project:** ${input.projectName}
2549
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2550
+ **Parent task:** ${parentTaskId}` : ""}
2551
+ **Reviewer:** ${reviewer}
2552
+
2553
+ ## Context
2554
+
2555
+ ${input.context}
2556
+
2557
+ ## MANDATORY: When done
2558
+
2559
+ You MUST call update_task with status "done" and a result summary when finished.
2560
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2561
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2562
+ `;
2563
+ await writeFile3(mdPath, mdContent, "utf-8");
2564
+ } catch {
2565
+ }
2566
+ }
1955
2567
  return {
1956
2568
  id,
1957
2569
  title: input.title,
@@ -2144,7 +2756,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2144
2756
  return { row, taskFile, now, taskId };
2145
2757
  }
2146
2758
  }
2147
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2759
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
2148
2760
  process.stderr.write(
2149
2761
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2150
2762
  `
@@ -2209,9 +2821,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2209
2821
  return { taskFile, assignedTo, assignedBy, taskSlug };
2210
2822
  }
2211
2823
  async function ensureArchitectureDoc(baseDir, projectName2) {
2212
- const archPath = path8.join(baseDir, "exe", "ARCHITECTURE.md");
2824
+ const archPath = path9.join(baseDir, "exe", "ARCHITECTURE.md");
2213
2825
  try {
2214
- if (existsSync8(archPath)) return;
2826
+ if (existsSync9(archPath)) return;
2215
2827
  const template = [
2216
2828
  `# ${projectName2} \u2014 System Architecture`,
2217
2829
  "",
@@ -2244,10 +2856,10 @@ async function ensureArchitectureDoc(baseDir, projectName2) {
2244
2856
  }
2245
2857
  }
2246
2858
  async function ensureGitignoreExe(baseDir) {
2247
- const gitignorePath = path8.join(baseDir, ".gitignore");
2859
+ const gitignorePath = path9.join(baseDir, ".gitignore");
2248
2860
  try {
2249
- if (existsSync8(gitignorePath)) {
2250
- const content = readFileSync8(gitignorePath, "utf-8");
2861
+ if (existsSync9(gitignorePath)) {
2862
+ const content = readFileSync9(gitignorePath, "utf-8");
2251
2863
  if (/^\/?exe\/?$/m.test(content)) return;
2252
2864
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
2253
2865
  } else {
@@ -2256,20 +2868,30 @@ async function ensureGitignoreExe(baseDir) {
2256
2868
  } catch {
2257
2869
  }
2258
2870
  }
2259
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2871
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2260
2872
  var init_tasks_crud = __esm({
2261
2873
  "src/lib/tasks-crud.ts"() {
2262
2874
  "use strict";
2263
2875
  init_database();
2264
2876
  init_task_scope();
2877
+ init_employees();
2878
+ LANE_KEYWORDS = {
2879
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
2880
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
2881
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
2882
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
2883
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
2884
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
2885
+ };
2886
+ KEYWORD_INDEX = buildKeywordIndex();
2265
2887
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2266
2888
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2267
2889
  }
2268
2890
  });
2269
2891
 
2270
2892
  // src/lib/tasks-review.ts
2271
- import path9 from "path";
2272
- import { existsSync as existsSync9, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
2893
+ import path10 from "path";
2894
+ import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
2273
2895
  async function countPendingReviews(sessionScope) {
2274
2896
  const client = getClient();
2275
2897
  if (sessionScope) {
@@ -2291,7 +2913,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2291
2913
  const result2 = await client.execute({
2292
2914
  sql: `SELECT COUNT(*) as cnt FROM tasks
2293
2915
  WHERE status = 'needs_review' AND updated_at > ?
2294
- AND (session_scope = ? OR session_scope IS NULL)`,
2916
+ AND session_scope = ?`,
2295
2917
  args: [sinceIso, sessionScope]
2296
2918
  });
2297
2919
  return Number(result2.rows[0]?.cnt) || 0;
@@ -2309,7 +2931,7 @@ async function listPendingReviews(limit, sessionScope) {
2309
2931
  const result2 = await client.execute({
2310
2932
  sql: `SELECT title, assigned_to, project_name FROM tasks
2311
2933
  WHERE status = 'needs_review'
2312
- AND (session_scope = ? OR session_scope IS NULL)
2934
+ AND session_scope = ?
2313
2935
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
2314
2936
  args: [sessionScope, limit]
2315
2937
  });
@@ -2430,14 +3052,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2430
3052
  if (parts.length >= 3 && parts[0] === "review") {
2431
3053
  const agent = parts[1];
2432
3054
  const slug = parts.slice(2).join("-");
2433
- const originalTaskFile = `exe/${agent}/${slug}.md`;
3055
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
2434
3056
  const result = await client.execute({
2435
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
2436
- args: [now, originalTaskFile]
3057
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
3058
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
2437
3059
  });
2438
3060
  if (result.rowsAffected > 0) {
2439
3061
  process.stderr.write(
2440
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
3062
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
2441
3063
  `
2442
3064
  );
2443
3065
  }
@@ -2450,11 +3072,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2450
3072
  );
2451
3073
  }
2452
3074
  try {
2453
- const cacheDir = path9.join(EXE_AI_DIR, "session-cache");
2454
- if (existsSync9(cacheDir)) {
3075
+ const cacheDir = path10.join(EXE_AI_DIR, "session-cache");
3076
+ if (existsSync10(cacheDir)) {
2455
3077
  for (const f of readdirSync2(cacheDir)) {
2456
3078
  if (f.startsWith("review-notified-")) {
2457
- unlinkSync3(path9.join(cacheDir, f));
3079
+ unlinkSync4(path10.join(cacheDir, f));
2458
3080
  }
2459
3081
  }
2460
3082
  }
@@ -2475,7 +3097,7 @@ var init_tasks_review = __esm({
2475
3097
  });
2476
3098
 
2477
3099
  // src/lib/tasks-chain.ts
2478
- import path10 from "path";
3100
+ import path11 from "path";
2479
3101
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2480
3102
  async function cascadeUnblock(taskId, baseDir, now) {
2481
3103
  const client = getClient();
@@ -2492,7 +3114,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
2492
3114
  });
2493
3115
  for (const ur of unblockedRows.rows) {
2494
3116
  try {
2495
- const ubFile = path10.join(baseDir, String(ur.task_file));
3117
+ const ubFile = path11.join(baseDir, String(ur.task_file));
2496
3118
  let ubContent = await readFile3(ubFile, "utf-8");
2497
3119
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
2498
3120
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -2561,7 +3183,7 @@ var init_tasks_chain = __esm({
2561
3183
 
2562
3184
  // src/lib/project-name.ts
2563
3185
  import { execSync as execSync5 } from "child_process";
2564
- import path11 from "path";
3186
+ import path12 from "path";
2565
3187
  function getProjectName(cwd) {
2566
3188
  const dir = cwd ?? process.cwd();
2567
3189
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2574,7 +3196,7 @@ function getProjectName(cwd) {
2574
3196
  timeout: 2e3,
2575
3197
  stdio: ["pipe", "pipe", "pipe"]
2576
3198
  }).trim();
2577
- repoRoot = path11.dirname(gitCommonDir);
3199
+ repoRoot = path12.dirname(gitCommonDir);
2578
3200
  } catch {
2579
3201
  repoRoot = execSync5("git rev-parse --show-toplevel", {
2580
3202
  cwd: dir,
@@ -2583,11 +3205,11 @@ function getProjectName(cwd) {
2583
3205
  stdio: ["pipe", "pipe", "pipe"]
2584
3206
  }).trim();
2585
3207
  }
2586
- _cached2 = path11.basename(repoRoot);
3208
+ _cached2 = path12.basename(repoRoot);
2587
3209
  _cachedCwd = dir;
2588
3210
  return _cached2;
2589
3211
  } catch {
2590
- _cached2 = path11.basename(dir);
3212
+ _cached2 = path12.basename(dir);
2591
3213
  _cachedCwd = dir;
2592
3214
  return _cached2;
2593
3215
  }
@@ -2619,7 +3241,7 @@ function findSessionForProject(projectName2) {
2619
3241
  const sessions = listSessions();
2620
3242
  for (const s of sessions) {
2621
3243
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2622
- if (proj === projectName2 && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
3244
+ if (proj === projectName2 && isCoordinatorName(s.agentId)) return s;
2623
3245
  }
2624
3246
  return null;
2625
3247
  }
@@ -2665,7 +3287,7 @@ var init_session_scope = __esm({
2665
3287
 
2666
3288
  // src/lib/tasks-notify.ts
2667
3289
  async function dispatchTaskToEmployee(input) {
2668
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
3290
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2669
3291
  let crossProject = false;
2670
3292
  if (input.projectName) {
2671
3293
  try {
@@ -3060,8 +3682,8 @@ __export(tasks_exports, {
3060
3682
  updateTaskStatus: () => updateTaskStatus,
3061
3683
  writeCheckpoint: () => writeCheckpoint
3062
3684
  });
3063
- import path12 from "path";
3064
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4 } from "fs";
3685
+ import path13 from "path";
3686
+ import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync5 } from "fs";
3065
3687
  async function createTask(input) {
3066
3688
  const result = await createTaskCore(input);
3067
3689
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -3080,14 +3702,14 @@ async function updateTask(input) {
3080
3702
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
3081
3703
  try {
3082
3704
  const agent = String(row.assigned_to);
3083
- const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3084
- const cachePath = path12.join(cacheDir, `current-task-${agent}.json`);
3705
+ const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3706
+ const cachePath = path13.join(cacheDir, `current-task-${agent}.json`);
3085
3707
  if (input.status === "in_progress") {
3086
3708
  mkdirSync4(cacheDir, { recursive: true });
3087
3709
  writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3088
3710
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3089
3711
  try {
3090
- unlinkSync4(cachePath);
3712
+ unlinkSync5(cachePath);
3091
3713
  } catch {
3092
3714
  }
3093
3715
  }
@@ -3144,7 +3766,7 @@ async function updateTask(input) {
3144
3766
  }
3145
3767
  const isTerminal = input.status === "done" || input.status === "needs_review";
3146
3768
  if (isTerminal) {
3147
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3769
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
3148
3770
  if (!isCoordinator) {
3149
3771
  notifyTaskDone();
3150
3772
  }
@@ -3169,7 +3791,7 @@ async function updateTask(input) {
3169
3791
  }
3170
3792
  }
3171
3793
  }
3172
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3794
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3173
3795
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3174
3796
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3175
3797
  taskId,
@@ -3185,7 +3807,7 @@ async function updateTask(input) {
3185
3807
  });
3186
3808
  }
3187
3809
  let nextTask;
3188
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3810
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
3189
3811
  try {
3190
3812
  nextTask = await findNextTask(String(row.assigned_to));
3191
3813
  } catch {
@@ -3529,7 +4151,7 @@ var init_capacity_monitor = __esm({
3529
4151
  // src/lib/tmux-routing.ts
3530
4152
  var tmux_routing_exports = {};
3531
4153
  __export(tmux_routing_exports, {
3532
- acquireSpawnLock: () => acquireSpawnLock,
4154
+ acquireSpawnLock: () => acquireSpawnLock2,
3533
4155
  employeeSessionName: () => employeeSessionName,
3534
4156
  ensureEmployee: () => ensureEmployee,
3535
4157
  extractRootExe: () => extractRootExe,
@@ -3544,20 +4166,20 @@ __export(tmux_routing_exports, {
3544
4166
  notifyParentExe: () => notifyParentExe,
3545
4167
  parseParentExe: () => parseParentExe,
3546
4168
  registerParentExe: () => registerParentExe,
3547
- releaseSpawnLock: () => releaseSpawnLock,
4169
+ releaseSpawnLock: () => releaseSpawnLock2,
3548
4170
  resolveExeSession: () => resolveExeSession,
3549
4171
  sendIntercom: () => sendIntercom,
3550
4172
  spawnEmployee: () => spawnEmployee,
3551
4173
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
3552
4174
  });
3553
4175
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
3554
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10, appendFileSync } from "fs";
3555
- import path13 from "path";
3556
- import os6 from "os";
3557
- import { fileURLToPath } from "url";
3558
- import { unlinkSync as unlinkSync5 } from "fs";
4176
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync11, appendFileSync } from "fs";
4177
+ import path14 from "path";
4178
+ import os7 from "os";
4179
+ import { fileURLToPath as fileURLToPath2 } from "url";
4180
+ import { unlinkSync as unlinkSync6 } from "fs";
3559
4181
  function spawnLockPath(sessionName) {
3560
- return path13.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4182
+ return path14.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3561
4183
  }
3562
4184
  function isProcessAlive(pid) {
3563
4185
  try {
@@ -3567,14 +4189,14 @@ function isProcessAlive(pid) {
3567
4189
  return false;
3568
4190
  }
3569
4191
  }
3570
- function acquireSpawnLock(sessionName) {
3571
- if (!existsSync10(SPAWN_LOCK_DIR)) {
4192
+ function acquireSpawnLock2(sessionName) {
4193
+ if (!existsSync11(SPAWN_LOCK_DIR)) {
3572
4194
  mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
3573
4195
  }
3574
4196
  const lockFile = spawnLockPath(sessionName);
3575
- if (existsSync10(lockFile)) {
4197
+ if (existsSync11(lockFile)) {
3576
4198
  try {
3577
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
4199
+ const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
3578
4200
  const age = Date.now() - lock.timestamp;
3579
4201
  if (isProcessAlive(lock.pid) && age < 6e4) {
3580
4202
  return false;
@@ -3585,22 +4207,22 @@ function acquireSpawnLock(sessionName) {
3585
4207
  writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
3586
4208
  return true;
3587
4209
  }
3588
- function releaseSpawnLock(sessionName) {
4210
+ function releaseSpawnLock2(sessionName) {
3589
4211
  try {
3590
- unlinkSync5(spawnLockPath(sessionName));
4212
+ unlinkSync6(spawnLockPath(sessionName));
3591
4213
  } catch {
3592
4214
  }
3593
4215
  }
3594
4216
  function resolveBehaviorsExporterScript() {
3595
4217
  try {
3596
- const thisFile = fileURLToPath(import.meta.url);
3597
- const scriptPath = path13.join(
3598
- path13.dirname(thisFile),
4218
+ const thisFile = fileURLToPath2(import.meta.url);
4219
+ const scriptPath = path14.join(
4220
+ path14.dirname(thisFile),
3599
4221
  "..",
3600
4222
  "bin",
3601
4223
  "exe-export-behaviors.js"
3602
4224
  );
3603
- return existsSync10(scriptPath) ? scriptPath : null;
4225
+ return existsSync11(scriptPath) ? scriptPath : null;
3604
4226
  } catch {
3605
4227
  return null;
3606
4228
  }
@@ -3666,11 +4288,11 @@ function extractRootExe(name) {
3666
4288
  return parts.length > 0 ? parts[parts.length - 1] : null;
3667
4289
  }
3668
4290
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3669
- if (!existsSync10(SESSION_CACHE)) {
4291
+ if (!existsSync11(SESSION_CACHE)) {
3670
4292
  mkdirSync5(SESSION_CACHE, { recursive: true });
3671
4293
  }
3672
4294
  const rootExe = extractRootExe(parentExe) ?? parentExe;
3673
- const filePath = path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4295
+ const filePath = path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3674
4296
  writeFileSync6(filePath, JSON.stringify({
3675
4297
  parentExe: rootExe,
3676
4298
  dispatchedBy: dispatchedBy || rootExe,
@@ -3679,7 +4301,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3679
4301
  }
3680
4302
  function getParentExe(sessionKey) {
3681
4303
  try {
3682
- const data = JSON.parse(readFileSync9(path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4304
+ const data = JSON.parse(readFileSync10(path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3683
4305
  return data.parentExe || null;
3684
4306
  } catch {
3685
4307
  return null;
@@ -3687,8 +4309,8 @@ function getParentExe(sessionKey) {
3687
4309
  }
3688
4310
  function getDispatchedBy(sessionKey) {
3689
4311
  try {
3690
- const data = JSON.parse(readFileSync9(
3691
- path13.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4312
+ const data = JSON.parse(readFileSync10(
4313
+ path14.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3692
4314
  "utf8"
3693
4315
  ));
3694
4316
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -3714,10 +4336,10 @@ function isEmployeeAlive(sessionName) {
3714
4336
  }
3715
4337
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
3716
4338
  const base = employeeSessionName(employeeName, exeSession);
3717
- if (!isAlive(base) && acquireSpawnLock(base)) return 0;
4339
+ if (!isAlive(base) && acquireSpawnLock2(base)) return 0;
3718
4340
  for (let i = 2; i <= maxInstances; i++) {
3719
4341
  const candidate = employeeSessionName(employeeName, exeSession, i);
3720
- if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
4342
+ if (!isAlive(candidate) && acquireSpawnLock2(candidate)) return i;
3721
4343
  }
3722
4344
  return null;
3723
4345
  }
@@ -3749,15 +4371,15 @@ async function verifyPaneAtCapacity(sessionName) {
3749
4371
  }
3750
4372
  function readDebounceState() {
3751
4373
  try {
3752
- if (!existsSync10(DEBOUNCE_FILE)) return {};
3753
- return JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
4374
+ if (!existsSync11(DEBOUNCE_FILE)) return {};
4375
+ return JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
3754
4376
  } catch {
3755
4377
  return {};
3756
4378
  }
3757
4379
  }
3758
4380
  function writeDebounceState(state) {
3759
4381
  try {
3760
- if (!existsSync10(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
4382
+ if (!existsSync11(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
3761
4383
  writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
3762
4384
  } catch {
3763
4385
  }
@@ -3877,7 +4499,7 @@ function notifyParentExe(sessionKey) {
3877
4499
  return true;
3878
4500
  }
3879
4501
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3880
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
4502
+ if (isCoordinatorName(employeeName)) {
3881
4503
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
3882
4504
  }
3883
4505
  try {
@@ -3949,26 +4571,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3949
4571
  const transport = getTransport();
3950
4572
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3951
4573
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3952
- const logDir = path13.join(os6.homedir(), ".exe-os", "session-logs");
3953
- const logFile = path13.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3954
- if (!existsSync10(logDir)) {
4574
+ const logDir = path14.join(os7.homedir(), ".exe-os", "session-logs");
4575
+ const logFile = path14.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4576
+ if (!existsSync11(logDir)) {
3955
4577
  mkdirSync5(logDir, { recursive: true });
3956
4578
  }
3957
4579
  transport.kill(sessionName);
3958
4580
  let cleanupSuffix = "";
3959
4581
  try {
3960
- const thisFile = fileURLToPath(import.meta.url);
3961
- const cleanupScript = path13.join(path13.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3962
- if (existsSync10(cleanupScript)) {
4582
+ const thisFile = fileURLToPath2(import.meta.url);
4583
+ const cleanupScript = path14.join(path14.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4584
+ if (existsSync11(cleanupScript)) {
3963
4585
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
3964
4586
  }
3965
4587
  } catch {
3966
4588
  }
3967
4589
  try {
3968
- const claudeJsonPath = path13.join(os6.homedir(), ".claude.json");
4590
+ const claudeJsonPath = path14.join(os7.homedir(), ".claude.json");
3969
4591
  let claudeJson = {};
3970
4592
  try {
3971
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
4593
+ claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
3972
4594
  } catch {
3973
4595
  }
3974
4596
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -3980,13 +4602,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3980
4602
  } catch {
3981
4603
  }
3982
4604
  try {
3983
- const settingsDir = path13.join(os6.homedir(), ".claude", "projects");
4605
+ const settingsDir = path14.join(os7.homedir(), ".claude", "projects");
3984
4606
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3985
- const projSettingsDir = path13.join(settingsDir, normalizedKey);
3986
- const settingsPath = path13.join(projSettingsDir, "settings.json");
4607
+ const projSettingsDir = path14.join(settingsDir, normalizedKey);
4608
+ const settingsPath = path14.join(projSettingsDir, "settings.json");
3987
4609
  let settings = {};
3988
4610
  try {
3989
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
4611
+ settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
3990
4612
  } catch {
3991
4613
  }
3992
4614
  const perms = settings.permissions ?? {};
@@ -4027,8 +4649,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4027
4649
  let behaviorsFlag = "";
4028
4650
  let legacyFallbackWarned = false;
4029
4651
  if (!useExeAgent && !useBinSymlink) {
4030
- const identityPath = path13.join(
4031
- os6.homedir(),
4652
+ const identityPath = path14.join(
4653
+ os7.homedir(),
4032
4654
  ".exe-os",
4033
4655
  "identity",
4034
4656
  `${employeeName}.md`
@@ -4037,13 +4659,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4037
4659
  const hasAgentFlag = claudeSupportsAgentFlag();
4038
4660
  if (hasAgentFlag) {
4039
4661
  identityFlag = ` --agent ${employeeName}`;
4040
- } else if (existsSync10(identityPath)) {
4662
+ } else if (existsSync11(identityPath)) {
4041
4663
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
4042
4664
  legacyFallbackWarned = true;
4043
4665
  }
4044
4666
  const behaviorsFile = exportBehaviorsSync(
4045
4667
  employeeName,
4046
- path13.basename(spawnCwd),
4668
+ path14.basename(spawnCwd),
4047
4669
  sessionName
4048
4670
  );
4049
4671
  if (behaviorsFile) {
@@ -4058,9 +4680,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4058
4680
  }
4059
4681
  let sessionContextFlag = "";
4060
4682
  try {
4061
- const ctxDir = path13.join(os6.homedir(), ".exe-os", "session-cache");
4683
+ const ctxDir = path14.join(os7.homedir(), ".exe-os", "session-cache");
4062
4684
  mkdirSync5(ctxDir, { recursive: true });
4063
- const ctxFile = path13.join(ctxDir, `session-context-${sessionName}.md`);
4685
+ const ctxFile = path14.join(ctxDir, `session-context-${sessionName}.md`);
4064
4686
  const ctxContent = [
4065
4687
  `## Session Context`,
4066
4688
  `You are running in tmux session: ${sessionName}.`,
@@ -4099,13 +4721,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4099
4721
  command: spawnCommand
4100
4722
  });
4101
4723
  if (spawnResult.error) {
4102
- releaseSpawnLock(sessionName);
4724
+ releaseSpawnLock2(sessionName);
4103
4725
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
4104
4726
  }
4105
4727
  transport.pipeLog(sessionName, logFile);
4106
4728
  try {
4107
4729
  const mySession = getMySession();
4108
- const dispatchInfo = path13.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4730
+ const dispatchInfo = path14.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4109
4731
  writeFileSync6(dispatchInfo, JSON.stringify({
4110
4732
  dispatchedBy: mySession,
4111
4733
  rootExe: exeSession,
@@ -4137,7 +4759,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4137
4759
  }
4138
4760
  }
4139
4761
  if (!booted) {
4140
- releaseSpawnLock(sessionName);
4762
+ releaseSpawnLock2(sessionName);
4141
4763
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4142
4764
  }
4143
4765
  if (!useExeAgent) {
@@ -4154,7 +4776,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4154
4776
  pid: 0,
4155
4777
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
4156
4778
  });
4157
- releaseSpawnLock(sessionName);
4779
+ releaseSpawnLock2(sessionName);
4158
4780
  return { sessionName };
4159
4781
  }
4160
4782
  var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
@@ -4170,14 +4792,14 @@ var init_tmux_routing = __esm({
4170
4792
  init_intercom_queue();
4171
4793
  init_plan_limits();
4172
4794
  init_employees();
4173
- SPAWN_LOCK_DIR = path13.join(os6.homedir(), ".exe-os", "spawn-locks");
4174
- SESSION_CACHE = path13.join(os6.homedir(), ".exe-os", "session-cache");
4795
+ SPAWN_LOCK_DIR = path14.join(os7.homedir(), ".exe-os", "spawn-locks");
4796
+ SESSION_CACHE = path14.join(os7.homedir(), ".exe-os", "session-cache");
4175
4797
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4176
4798
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
4177
4799
  VERIFY_PANE_LINES = 200;
4178
4800
  INTERCOM_DEBOUNCE_MS = 3e4;
4179
- INTERCOM_LOG2 = path13.join(os6.homedir(), ".exe-os", "intercom.log");
4180
- DEBOUNCE_FILE = path13.join(SESSION_CACHE, "intercom-debounce.json");
4801
+ INTERCOM_LOG2 = path14.join(os7.homedir(), ".exe-os", "intercom.log");
4802
+ DEBOUNCE_FILE = path14.join(SESSION_CACHE, "intercom-debounce.json");
4181
4803
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4182
4804
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
4183
4805
  }
@@ -4218,14 +4840,14 @@ var init_memory = __esm({
4218
4840
 
4219
4841
  // src/lib/keychain.ts
4220
4842
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
4221
- import { existsSync as existsSync11 } from "fs";
4222
- import path14 from "path";
4223
- import os7 from "os";
4843
+ import { existsSync as existsSync12 } from "fs";
4844
+ import path15 from "path";
4845
+ import os8 from "os";
4224
4846
  function getKeyDir() {
4225
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path14.join(os7.homedir(), ".exe-os");
4847
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path15.join(os8.homedir(), ".exe-os");
4226
4848
  }
4227
4849
  function getKeyPath() {
4228
- return path14.join(getKeyDir(), "master.key");
4850
+ return path15.join(getKeyDir(), "master.key");
4229
4851
  }
4230
4852
  async function tryKeytar() {
4231
4853
  try {
@@ -4246,13 +4868,21 @@ async function getMasterKey() {
4246
4868
  }
4247
4869
  }
4248
4870
  const keyPath = getKeyPath();
4249
- if (!existsSync11(keyPath)) {
4871
+ if (!existsSync12(keyPath)) {
4872
+ process.stderr.write(
4873
+ `[keychain] Key not found at ${keyPath} (HOME=${os8.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
4874
+ `
4875
+ );
4250
4876
  return null;
4251
4877
  }
4252
4878
  try {
4253
4879
  const content = await readFile4(keyPath, "utf-8");
4254
4880
  return Buffer.from(content.trim(), "base64");
4255
- } catch {
4881
+ } catch (err) {
4882
+ process.stderr.write(
4883
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
4884
+ `
4885
+ );
4256
4886
  return null;
4257
4887
  }
4258
4888
  }
@@ -4278,12 +4908,12 @@ __export(shard_manager_exports, {
4278
4908
  listShards: () => listShards,
4279
4909
  shardExists: () => shardExists
4280
4910
  });
4281
- import path15 from "path";
4282
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync3 } from "fs";
4911
+ import path16 from "path";
4912
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync3 } from "fs";
4283
4913
  import { createClient as createClient2 } from "@libsql/client";
4284
4914
  function initShardManager(encryptionKey) {
4285
4915
  _encryptionKey = encryptionKey;
4286
- if (!existsSync12(SHARDS_DIR)) {
4916
+ if (!existsSync13(SHARDS_DIR)) {
4287
4917
  mkdirSync6(SHARDS_DIR, { recursive: true });
4288
4918
  }
4289
4919
  _shardingEnabled = true;
@@ -4304,7 +4934,7 @@ function getShardClient(projectName2) {
4304
4934
  }
4305
4935
  const cached = _shards.get(safeName);
4306
4936
  if (cached) return cached;
4307
- const dbPath = path15.join(SHARDS_DIR, `${safeName}.db`);
4937
+ const dbPath = path16.join(SHARDS_DIR, `${safeName}.db`);
4308
4938
  const client = createClient2({
4309
4939
  url: `file:${dbPath}`,
4310
4940
  encryptionKey: _encryptionKey
@@ -4314,10 +4944,10 @@ function getShardClient(projectName2) {
4314
4944
  }
4315
4945
  function shardExists(projectName2) {
4316
4946
  const safeName = projectName2.replace(/[^a-zA-Z0-9_-]/g, "_");
4317
- return existsSync12(path15.join(SHARDS_DIR, `${safeName}.db`));
4947
+ return existsSync13(path16.join(SHARDS_DIR, `${safeName}.db`));
4318
4948
  }
4319
4949
  function listShards() {
4320
- if (!existsSync12(SHARDS_DIR)) return [];
4950
+ if (!existsSync13(SHARDS_DIR)) return [];
4321
4951
  return readdirSync3(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
4322
4952
  }
4323
4953
  async function ensureShardSchema(client) {
@@ -4503,7 +5133,7 @@ var init_shard_manager = __esm({
4503
5133
  "src/lib/shard-manager.ts"() {
4504
5134
  "use strict";
4505
5135
  init_config();
4506
- SHARDS_DIR = path15.join(EXE_AI_DIR, "shards");
5136
+ SHARDS_DIR = path16.join(EXE_AI_DIR, "shards");
4507
5137
  _shards = /* @__PURE__ */ new Map();
4508
5138
  _encryptionKey = null;
4509
5139
  _shardingEnabled = false;
@@ -4628,7 +5258,7 @@ __export(global_procedures_exports, {
4628
5258
  loadGlobalProcedures: () => loadGlobalProcedures,
4629
5259
  storeGlobalProcedure: () => storeGlobalProcedure
4630
5260
  });
4631
- import { randomUUID as randomUUID2 } from "crypto";
5261
+ import { randomUUID as randomUUID3 } from "crypto";
4632
5262
  async function loadGlobalProcedures() {
4633
5263
  const client = getClient();
4634
5264
  const result = await client.execute({
@@ -4657,7 +5287,7 @@ ${sections.join("\n\n")}
4657
5287
  `;
4658
5288
  }
4659
5289
  async function storeGlobalProcedure(input) {
4660
- const id = randomUUID2();
5290
+ const id = randomUUID3();
4661
5291
  const now = (/* @__PURE__ */ new Date()).toISOString();
4662
5292
  const client = getClient();
4663
5293
  await client.execute({
@@ -4708,6 +5338,7 @@ __export(store_exports, {
4708
5338
  vectorToBlob: () => vectorToBlob,
4709
5339
  writeMemory: () => writeMemory
4710
5340
  });
5341
+ import { createHash } from "crypto";
4711
5342
  function isBusyError2(err) {
4712
5343
  if (err instanceof Error) {
4713
5344
  const msg = err.message.toLowerCase();
@@ -4781,12 +5412,52 @@ function classifyTier(record) {
4781
5412
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
4782
5413
  return 3;
4783
5414
  }
5415
+ function inferFilePaths(record) {
5416
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
5417
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
5418
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
5419
+ return match ? JSON.stringify([match[1]]) : null;
5420
+ }
5421
+ function inferCommitHash(record) {
5422
+ if (record.tool_name !== "Bash") return null;
5423
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
5424
+ return match ? match[1] : null;
5425
+ }
5426
+ function inferLanguageType(record) {
5427
+ const text = record.raw_text;
5428
+ if (!text || text.length < 10) return null;
5429
+ const trimmed = text.trimStart();
5430
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
5431
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
5432
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
5433
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
5434
+ return "mixed";
5435
+ }
5436
+ function inferDomain(record) {
5437
+ const proj = (record.project_name ?? "").toLowerCase();
5438
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
5439
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
5440
+ return null;
5441
+ }
4784
5442
  async function writeMemory(record) {
4785
5443
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
4786
5444
  throw new Error(
4787
5445
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
4788
5446
  );
4789
5447
  }
5448
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
5449
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
5450
+ return;
5451
+ }
5452
+ try {
5453
+ const client = getClient();
5454
+ const existing = await client.execute({
5455
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
5456
+ args: [contentHash, record.agent_id]
5457
+ });
5458
+ if (existing.rows.length > 0) return;
5459
+ } catch {
5460
+ }
4790
5461
  const dbRow = {
4791
5462
  id: record.id,
4792
5463
  agent_id: record.agent_id,
@@ -4816,7 +5487,23 @@ async function writeMemory(record) {
4816
5487
  supersedes_id: record.supersedes_id ?? null,
4817
5488
  draft: record.draft ? 1 : 0,
4818
5489
  memory_type: record.memory_type ?? "raw",
4819
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
5490
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
5491
+ content_hash: contentHash,
5492
+ intent: record.intent ?? null,
5493
+ outcome: record.outcome ?? null,
5494
+ domain: record.domain ?? inferDomain(record),
5495
+ referenced_entities: record.referenced_entities ?? null,
5496
+ retrieval_count: record.retrieval_count ?? 0,
5497
+ chain_position: record.chain_position ?? null,
5498
+ review_status: record.review_status ?? null,
5499
+ context_window_pct: record.context_window_pct ?? null,
5500
+ file_paths: record.file_paths ?? inferFilePaths(record),
5501
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
5502
+ duration_ms: record.duration_ms ?? null,
5503
+ token_cost: record.token_cost ?? null,
5504
+ audience: record.audience ?? null,
5505
+ language_type: record.language_type ?? inferLanguageType(record),
5506
+ parent_memory_id: record.parent_memory_id ?? null
4820
5507
  };
4821
5508
  _pendingRecords.push(dbRow);
4822
5509
  orgBus.emit({
@@ -4874,80 +5561,85 @@ async function flushBatch() {
4874
5561
  const draft = row.draft ? 1 : 0;
4875
5562
  const memoryType = row.memory_type ?? "raw";
4876
5563
  const trajectory = row.trajectory ?? null;
4877
- return {
4878
- sql: hasVector ? `INSERT OR IGNORE INTO memories
4879
- (id, agent_id, agent_role, session_id, timestamp,
4880
- tool_name, project_name,
4881
- has_error, raw_text, vector, version, task_id, importance, status,
4882
- confidence, last_accessed,
4883
- workspace_id, document_id, user_id, char_offset, page_number,
4884
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
4885
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
4886
- (id, agent_id, agent_role, session_id, timestamp,
5564
+ const contentHash = row.content_hash ?? null;
5565
+ const intent = row.intent ?? null;
5566
+ const outcome = row.outcome ?? null;
5567
+ const domain = row.domain ?? null;
5568
+ const referencedEntities = row.referenced_entities ?? null;
5569
+ const retrievalCount = row.retrieval_count ?? 0;
5570
+ const chainPosition = row.chain_position ?? null;
5571
+ const reviewStatus = row.review_status ?? null;
5572
+ const contextWindowPct = row.context_window_pct ?? null;
5573
+ const filePaths = row.file_paths ?? null;
5574
+ const commitHash = row.commit_hash ?? null;
5575
+ const durationMs = row.duration_ms ?? null;
5576
+ const tokenCost = row.token_cost ?? null;
5577
+ const audience = row.audience ?? null;
5578
+ const languageType = row.language_type ?? null;
5579
+ const parentMemoryId = row.parent_memory_id ?? null;
5580
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
4887
5581
  tool_name, project_name,
4888
5582
  has_error, raw_text, vector, version, task_id, importance, status,
4889
5583
  confidence, last_accessed,
4890
5584
  workspace_id, document_id, user_id, char_offset, page_number,
4891
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
4892
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4893
- args: hasVector ? [
4894
- row.id,
4895
- row.agent_id,
4896
- row.agent_role,
4897
- row.session_id,
4898
- row.timestamp,
4899
- row.tool_name,
4900
- row.project_name,
4901
- row.has_error,
4902
- row.raw_text,
4903
- vectorToBlob(row.vector),
4904
- row.version,
4905
- taskId,
4906
- importance,
4907
- status,
4908
- confidence,
4909
- lastAccessed,
4910
- workspaceId,
4911
- documentId,
4912
- userId,
4913
- charOffset,
4914
- pageNumber,
4915
- sourcePath,
4916
- sourceType,
4917
- tier,
4918
- supersedesId,
4919
- draft,
4920
- memoryType,
4921
- trajectory
4922
- ] : [
4923
- row.id,
4924
- row.agent_id,
4925
- row.agent_role,
4926
- row.session_id,
4927
- row.timestamp,
4928
- row.tool_name,
4929
- row.project_name,
4930
- row.has_error,
4931
- row.raw_text,
4932
- row.version,
4933
- taskId,
4934
- importance,
4935
- status,
4936
- confidence,
4937
- lastAccessed,
4938
- workspaceId,
4939
- documentId,
4940
- userId,
4941
- charOffset,
4942
- pageNumber,
4943
- sourcePath,
4944
- sourceType,
4945
- tier,
4946
- supersedesId,
4947
- draft,
4948
- memoryType,
4949
- trajectory
4950
- ]
5585
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
5586
+ intent, outcome, domain, referenced_entities, retrieval_count,
5587
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
5588
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
5589
+ const metaArgs = [
5590
+ intent,
5591
+ outcome,
5592
+ domain,
5593
+ referencedEntities,
5594
+ retrievalCount,
5595
+ chainPosition,
5596
+ reviewStatus,
5597
+ contextWindowPct,
5598
+ filePaths,
5599
+ commitHash,
5600
+ durationMs,
5601
+ tokenCost,
5602
+ audience,
5603
+ languageType,
5604
+ parentMemoryId
5605
+ ];
5606
+ const baseArgs = [
5607
+ row.id,
5608
+ row.agent_id,
5609
+ row.agent_role,
5610
+ row.session_id,
5611
+ row.timestamp,
5612
+ row.tool_name,
5613
+ row.project_name,
5614
+ row.has_error,
5615
+ row.raw_text
5616
+ ];
5617
+ const sharedArgs = [
5618
+ row.version,
5619
+ taskId,
5620
+ importance,
5621
+ status,
5622
+ confidence,
5623
+ lastAccessed,
5624
+ workspaceId,
5625
+ documentId,
5626
+ userId,
5627
+ charOffset,
5628
+ pageNumber,
5629
+ sourcePath,
5630
+ sourceType,
5631
+ tier,
5632
+ supersedesId,
5633
+ draft,
5634
+ memoryType,
5635
+ trajectory,
5636
+ contentHash
5637
+ ];
5638
+ return {
5639
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
5640
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
5641
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5642
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
4951
5643
  };
4952
5644
  };
4953
5645
  const globalClient = getClient();
@@ -5402,13 +6094,13 @@ async function sweepTasks(projectName2, options = {}) {
5402
6094
  }
5403
6095
 
5404
6096
  // src/bin/git-sweep.ts
5405
- import path16 from "path";
6097
+ import path17 from "path";
5406
6098
  var args = process.argv.slice(2);
5407
6099
  var dryRun = args.includes("--dry-run");
5408
6100
  var projectIdx = args.indexOf("--project");
5409
6101
  var limitIdx = args.indexOf("--limit");
5410
6102
  var staleIdx = args.indexOf("--stale");
5411
- var projectName = projectIdx >= 0 ? args[projectIdx + 1] : process.cwd().split(path16.sep).pop();
6103
+ var projectName = projectIdx >= 0 ? args[projectIdx + 1] : process.cwd().split(path17.sep).pop();
5412
6104
  var commitLimit = limitIdx >= 0 ? Number(args[limitIdx + 1]) : void 0;
5413
6105
  var staleMinutes = staleIdx >= 0 ? Number(args[staleIdx + 1]) : void 0;
5414
6106
  async function main() {