@askexenow/exe-os 0.9.8 → 0.9.9

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 (101) hide show
  1. package/dist/bin/backfill-conversations.js +222 -49
  2. package/dist/bin/backfill-responses.js +221 -48
  3. package/dist/bin/backfill-vectors.js +225 -52
  4. package/dist/bin/cleanup-stale-review-tasks.js +150 -28
  5. package/dist/bin/cli.js +1295 -856
  6. package/dist/bin/exe-agent-config.js +36 -8
  7. package/dist/bin/exe-agent.js +14 -4
  8. package/dist/bin/exe-assign.js +221 -48
  9. package/dist/bin/exe-boot.js +778 -427
  10. package/dist/bin/exe-call.js +41 -13
  11. package/dist/bin/exe-cloud.js +163 -58
  12. package/dist/bin/exe-dispatch.js +276 -139
  13. package/dist/bin/exe-doctor.js +145 -27
  14. package/dist/bin/exe-export-behaviors.js +141 -23
  15. package/dist/bin/exe-forget.js +137 -19
  16. package/dist/bin/exe-gateway.js +677 -388
  17. package/dist/bin/exe-heartbeat.js +227 -108
  18. package/dist/bin/exe-kill.js +138 -20
  19. package/dist/bin/exe-launch-agent.js +172 -39
  20. package/dist/bin/exe-link.js +291 -100
  21. package/dist/bin/exe-new-employee.js +214 -106
  22. package/dist/bin/exe-pending-messages.js +395 -33
  23. package/dist/bin/exe-pending-notifications.js +684 -99
  24. package/dist/bin/exe-pending-reviews.js +420 -74
  25. package/dist/bin/exe-rename.js +147 -49
  26. package/dist/bin/exe-review.js +138 -20
  27. package/dist/bin/exe-search.js +240 -69
  28. package/dist/bin/exe-session-cleanup.js +440 -250
  29. package/dist/bin/exe-settings.js +61 -17
  30. package/dist/bin/exe-start-codex.js +158 -39
  31. package/dist/bin/exe-start-opencode.js +157 -38
  32. package/dist/bin/exe-status.js +151 -29
  33. package/dist/bin/exe-team.js +138 -20
  34. package/dist/bin/git-sweep.js +404 -212
  35. package/dist/bin/graph-backfill.js +137 -19
  36. package/dist/bin/graph-export.js +140 -22
  37. package/dist/bin/install.js +90 -61
  38. package/dist/bin/scan-tasks.js +412 -220
  39. package/dist/bin/setup.js +564 -293
  40. package/dist/bin/shard-migrate.js +139 -21
  41. package/dist/bin/update.js +138 -49
  42. package/dist/bin/wiki-sync.js +137 -19
  43. package/dist/gateway/index.js +533 -320
  44. package/dist/hooks/bug-report-worker.js +344 -193
  45. package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
  46. package/dist/hooks/commit-complete.js +402 -210
  47. package/dist/hooks/error-recall.js +245 -74
  48. package/dist/hooks/exe-heartbeat-hook.js +16 -6
  49. package/dist/hooks/ingest-worker.js +3423 -3157
  50. package/dist/hooks/ingest.js +832 -97
  51. package/dist/hooks/instructions-loaded.js +227 -54
  52. package/dist/hooks/notification.js +216 -43
  53. package/dist/hooks/post-compact.js +239 -62
  54. package/dist/hooks/pre-compact.js +408 -216
  55. package/dist/hooks/pre-tool-use.js +268 -90
  56. package/dist/hooks/prompt-ingest-worker.js +352 -102
  57. package/dist/hooks/prompt-submit.js +541 -328
  58. package/dist/hooks/response-ingest-worker.js +372 -122
  59. package/dist/hooks/session-end.js +443 -240
  60. package/dist/hooks/session-start.js +313 -127
  61. package/dist/hooks/stop.js +293 -98
  62. package/dist/hooks/subagent-stop.js +239 -62
  63. package/dist/hooks/summary-worker.js +568 -236
  64. package/dist/index.js +538 -324
  65. package/dist/lib/agent-config.js +28 -6
  66. package/dist/lib/cloud-sync.js +284 -105
  67. package/dist/lib/config.js +30 -10
  68. package/dist/lib/consolidation.js +16 -6
  69. package/dist/lib/database.js +123 -25
  70. package/dist/lib/db-daemon-client.js +73 -19
  71. package/dist/lib/db.js +123 -25
  72. package/dist/lib/device-registry.js +133 -35
  73. package/dist/lib/embedder.js +107 -32
  74. package/dist/lib/employee-templates.js +14 -4
  75. package/dist/lib/employees.js +41 -13
  76. package/dist/lib/exe-daemon-client.js +88 -22
  77. package/dist/lib/exe-daemon.js +935 -587
  78. package/dist/lib/hybrid-search.js +240 -69
  79. package/dist/lib/identity.js +18 -8
  80. package/dist/lib/license.js +133 -48
  81. package/dist/lib/messaging.js +116 -56
  82. package/dist/lib/reminders.js +14 -4
  83. package/dist/lib/schedules.js +137 -19
  84. package/dist/lib/skill-learning.js +33 -6
  85. package/dist/lib/store.js +137 -19
  86. package/dist/lib/task-router.js +14 -4
  87. package/dist/lib/tasks.js +280 -234
  88. package/dist/lib/tmux-routing.js +172 -125
  89. package/dist/lib/token-spend.js +26 -8
  90. package/dist/mcp/server.js +1326 -609
  91. package/dist/mcp/tools/complete-reminder.js +14 -4
  92. package/dist/mcp/tools/create-reminder.js +14 -4
  93. package/dist/mcp/tools/create-task.js +306 -248
  94. package/dist/mcp/tools/deactivate-behavior.js +16 -6
  95. package/dist/mcp/tools/list-reminders.js +14 -4
  96. package/dist/mcp/tools/list-tasks.js +123 -107
  97. package/dist/mcp/tools/send-message.js +75 -29
  98. package/dist/mcp/tools/update-task.js +1848 -199
  99. package/dist/runtime/index.js +441 -248
  100. package/dist/tui/App.js +761 -424
  101. package/package.json +1 -1
@@ -25,9 +25,47 @@ var __copyProps = (to, from, except, desc) => {
25
25
  };
26
26
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
27
 
28
+ // src/lib/secure-files.ts
29
+ import { chmodSync, existsSync, mkdirSync } from "fs";
30
+ import { chmod, mkdir } from "fs/promises";
31
+ async function ensurePrivateDir(dirPath) {
32
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
33
+ try {
34
+ await chmod(dirPath, PRIVATE_DIR_MODE);
35
+ } catch {
36
+ }
37
+ }
38
+ function ensurePrivateDirSync(dirPath) {
39
+ mkdirSync(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
40
+ try {
41
+ chmodSync(dirPath, PRIVATE_DIR_MODE);
42
+ } catch {
43
+ }
44
+ }
45
+ async function enforcePrivateFile(filePath) {
46
+ try {
47
+ await chmod(filePath, PRIVATE_FILE_MODE);
48
+ } catch {
49
+ }
50
+ }
51
+ function enforcePrivateFileSync(filePath) {
52
+ try {
53
+ if (existsSync(filePath)) chmodSync(filePath, PRIVATE_FILE_MODE);
54
+ } catch {
55
+ }
56
+ }
57
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
58
+ var init_secure_files = __esm({
59
+ "src/lib/secure-files.ts"() {
60
+ "use strict";
61
+ PRIVATE_DIR_MODE = 448;
62
+ PRIVATE_FILE_MODE = 384;
63
+ }
64
+ });
65
+
28
66
  // src/lib/config.ts
29
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
30
- import { readFileSync, existsSync, renameSync } from "fs";
67
+ import { readFile, writeFile } from "fs/promises";
68
+ import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
31
69
  import path2 from "path";
32
70
  import os2 from "os";
33
71
  function resolveDataDir() {
@@ -35,7 +73,7 @@ function resolveDataDir() {
35
73
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
36
74
  const newDir = path2.join(os2.homedir(), ".exe-os");
37
75
  const legacyDir = path2.join(os2.homedir(), ".exe-mem");
38
- if (!existsSync(newDir) && existsSync(legacyDir)) {
76
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
39
77
  try {
40
78
  renameSync(legacyDir, newDir);
41
79
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -98,9 +136,9 @@ function normalizeAutoUpdate(raw) {
98
136
  }
99
137
  async function loadConfig() {
100
138
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
101
- await mkdir(dir, { recursive: true });
139
+ await ensurePrivateDir(dir);
102
140
  const configPath = path2.join(dir, "config.json");
103
- if (!existsSync(configPath)) {
141
+ if (!existsSync2(configPath)) {
104
142
  return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
105
143
  }
106
144
  const raw = await readFile(configPath, "utf-8");
@@ -113,6 +151,7 @@ async function loadConfig() {
113
151
  `);
114
152
  try {
115
153
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
154
+ await enforcePrivateFile(configPath);
116
155
  } catch {
117
156
  }
118
157
  }
@@ -132,6 +171,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
132
171
  var init_config = __esm({
133
172
  "src/lib/config.ts"() {
134
173
  "use strict";
174
+ init_secure_files();
135
175
  EXE_AI_DIR = resolveDataDir();
136
176
  DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
137
177
  MODELS_DIR = path2.join(EXE_AI_DIR, "models");
@@ -210,7 +250,7 @@ var init_config = __esm({
210
250
 
211
251
  // src/lib/employees.ts
212
252
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
213
- import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
253
+ import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
214
254
  import { execSync } from "child_process";
215
255
  import path3 from "path";
216
256
  import os3 from "os";
@@ -231,7 +271,7 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
231
271
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
232
272
  }
233
273
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
234
- if (!existsSync2(employeesPath)) return [];
274
+ if (!existsSync3(employeesPath)) return [];
235
275
  try {
236
276
  return JSON.parse(readFileSync2(employeesPath, "utf-8"));
237
277
  } catch {
@@ -275,14 +315,14 @@ __export(session_registry_exports, {
275
315
  pruneStaleSessions: () => pruneStaleSessions,
276
316
  registerSession: () => registerSession
277
317
  });
278
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync3 } from "fs";
318
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
279
319
  import { execSync as execSync2 } from "child_process";
280
320
  import path4 from "path";
281
321
  import os4 from "os";
282
322
  function registerSession(entry) {
283
323
  const dir = path4.dirname(REGISTRY_PATH);
284
- if (!existsSync3(dir)) {
285
- mkdirSync(dir, { recursive: true });
324
+ if (!existsSync4(dir)) {
325
+ mkdirSync2(dir, { recursive: true });
286
326
  }
287
327
  const sessions = listSessions();
288
328
  const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
@@ -614,10 +654,10 @@ var init_runtime_table = __esm({
614
654
  });
615
655
 
616
656
  // src/lib/agent-config.ts
617
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
657
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
618
658
  import path5 from "path";
619
659
  function loadAgentConfig() {
620
- if (!existsSync4(AGENT_CONFIG_PATH)) return {};
660
+ if (!existsSync5(AGENT_CONFIG_PATH)) return {};
621
661
  try {
622
662
  return JSON.parse(readFileSync4(AGENT_CONFIG_PATH, "utf-8"));
623
663
  } catch {
@@ -638,6 +678,7 @@ var init_agent_config = __esm({
638
678
  "use strict";
639
679
  init_config();
640
680
  init_runtime_table();
681
+ init_secure_files();
641
682
  AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
642
683
  DEFAULT_MODELS = {
643
684
  claude: "claude-opus-4",
@@ -656,16 +697,16 @@ __export(intercom_queue_exports, {
656
697
  queueIntercom: () => queueIntercom,
657
698
  readQueue: () => readQueue
658
699
  });
659
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
700
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
660
701
  import path6 from "path";
661
702
  import os5 from "os";
662
703
  function ensureDir() {
663
704
  const dir = path6.dirname(QUEUE_PATH);
664
- if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
705
+ if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
665
706
  }
666
707
  function readQueue() {
667
708
  try {
668
- if (!existsSync5(QUEUE_PATH)) return [];
709
+ if (!existsSync6(QUEUE_PATH)) return [];
669
710
  return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
670
711
  } catch {
671
712
  return [];
@@ -1412,13 +1453,50 @@ var init_database_adapter = __esm({
1412
1453
  }
1413
1454
  });
1414
1455
 
1456
+ // src/lib/daemon-auth.ts
1457
+ import crypto from "crypto";
1458
+ import path8 from "path";
1459
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1460
+ function normalizeToken(token) {
1461
+ if (!token) return null;
1462
+ const trimmed = token.trim();
1463
+ return trimmed.length > 0 ? trimmed : null;
1464
+ }
1465
+ function readDaemonToken() {
1466
+ try {
1467
+ if (!existsSync7(DAEMON_TOKEN_PATH)) return null;
1468
+ return normalizeToken(readFileSync6(DAEMON_TOKEN_PATH, "utf8"));
1469
+ } catch {
1470
+ return null;
1471
+ }
1472
+ }
1473
+ function ensureDaemonToken(seed) {
1474
+ const existing = readDaemonToken();
1475
+ if (existing) return existing;
1476
+ const token = normalizeToken(seed) ?? crypto.randomBytes(32).toString("hex");
1477
+ ensurePrivateDirSync(EXE_AI_DIR);
1478
+ writeFileSync5(DAEMON_TOKEN_PATH, `${token}
1479
+ `, "utf8");
1480
+ enforcePrivateFileSync(DAEMON_TOKEN_PATH);
1481
+ return token;
1482
+ }
1483
+ var DAEMON_TOKEN_PATH;
1484
+ var init_daemon_auth = __esm({
1485
+ "src/lib/daemon-auth.ts"() {
1486
+ "use strict";
1487
+ init_config();
1488
+ init_secure_files();
1489
+ DAEMON_TOKEN_PATH = path8.join(EXE_AI_DIR, "exed.token");
1490
+ }
1491
+ });
1492
+
1415
1493
  // src/lib/exe-daemon-client.ts
1416
1494
  import net from "net";
1417
1495
  import os7 from "os";
1418
1496
  import { spawn } from "child_process";
1419
1497
  import { randomUUID as randomUUID2 } from "crypto";
1420
- import { existsSync as existsSync6, unlinkSync as unlinkSync2, readFileSync as readFileSync6, openSync, closeSync, statSync } from "fs";
1421
- import path8 from "path";
1498
+ import { existsSync as existsSync8, unlinkSync as unlinkSync2, readFileSync as readFileSync7, openSync, closeSync, statSync } from "fs";
1499
+ import path9 from "path";
1422
1500
  import { fileURLToPath } from "url";
1423
1501
  function handleData(chunk) {
1424
1502
  _buffer += chunk.toString();
@@ -1446,9 +1524,9 @@ function handleData(chunk) {
1446
1524
  }
1447
1525
  }
1448
1526
  function cleanupStaleFiles() {
1449
- if (existsSync6(PID_PATH)) {
1527
+ if (existsSync8(PID_PATH)) {
1450
1528
  try {
1451
- const pid = parseInt(readFileSync6(PID_PATH, "utf8").trim(), 10);
1529
+ const pid = parseInt(readFileSync7(PID_PATH, "utf8").trim(), 10);
1452
1530
  if (pid > 0) {
1453
1531
  try {
1454
1532
  process.kill(pid, 0);
@@ -1469,11 +1547,11 @@ function cleanupStaleFiles() {
1469
1547
  }
1470
1548
  }
1471
1549
  function findPackageRoot() {
1472
- let dir = path8.dirname(fileURLToPath(import.meta.url));
1473
- const { root } = path8.parse(dir);
1550
+ let dir = path9.dirname(fileURLToPath(import.meta.url));
1551
+ const { root } = path9.parse(dir);
1474
1552
  while (dir !== root) {
1475
- if (existsSync6(path8.join(dir, "package.json"))) return dir;
1476
- dir = path8.dirname(dir);
1553
+ if (existsSync8(path9.join(dir, "package.json"))) return dir;
1554
+ dir = path9.dirname(dir);
1477
1555
  }
1478
1556
  return null;
1479
1557
  }
@@ -1499,16 +1577,17 @@ function spawnDaemon() {
1499
1577
  process.stderr.write("[exed-client] WARN: cannot find package root\n");
1500
1578
  return;
1501
1579
  }
1502
- const daemonPath = path8.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1503
- if (!existsSync6(daemonPath)) {
1580
+ const daemonPath = path9.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1581
+ if (!existsSync8(daemonPath)) {
1504
1582
  process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
1505
1583
  `);
1506
1584
  return;
1507
1585
  }
1508
1586
  const resolvedPath = daemonPath;
1587
+ const daemonToken = ensureDaemonToken(process.env[DAEMON_TOKEN_ENV] ?? null);
1509
1588
  process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
1510
1589
  `);
1511
- const logPath = path8.join(path8.dirname(SOCKET_PATH), "exed.log");
1590
+ const logPath = path9.join(path9.dirname(SOCKET_PATH), "exed.log");
1512
1591
  let stderrFd = "ignore";
1513
1592
  try {
1514
1593
  stderrFd = openSync(logPath, "a");
@@ -1526,7 +1605,8 @@ function spawnDaemon() {
1526
1605
  TMUX_PANE: void 0,
1527
1606
  // Prevents resolveExeSession() from scoping to one session
1528
1607
  EXE_DAEMON_SOCK: SOCKET_PATH,
1529
- EXE_DAEMON_PID: PID_PATH
1608
+ EXE_DAEMON_PID: PID_PATH,
1609
+ [DAEMON_TOKEN_ENV]: daemonToken
1530
1610
  }
1531
1611
  });
1532
1612
  child.unref();
@@ -1633,13 +1713,14 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1633
1713
  return;
1634
1714
  }
1635
1715
  const id = randomUUID2();
1716
+ const token = process.env[DAEMON_TOKEN_ENV] ?? readDaemonToken();
1636
1717
  const timer = setTimeout(() => {
1637
1718
  _pending.delete(id);
1638
1719
  resolve({ error: "Request timeout" });
1639
1720
  }, timeoutMs);
1640
1721
  _pending.set(id, { resolve, timer });
1641
1722
  try {
1642
- _socket.write(JSON.stringify({ id, ...payload }) + "\n");
1723
+ _socket.write(JSON.stringify({ id, token, ...payload }) + "\n");
1643
1724
  } catch {
1644
1725
  clearTimeout(timer);
1645
1726
  _pending.delete(id);
@@ -1650,17 +1731,19 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1650
1731
  function isClientConnected() {
1651
1732
  return _connected;
1652
1733
  }
1653
- var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
1734
+ var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, DAEMON_TOKEN_ENV, _socket, _connected, _buffer, _pending, MAX_BUFFER;
1654
1735
  var init_exe_daemon_client = __esm({
1655
1736
  "src/lib/exe-daemon-client.ts"() {
1656
1737
  "use strict";
1657
1738
  init_config();
1658
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path8.join(EXE_AI_DIR, "exed.sock");
1659
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path8.join(EXE_AI_DIR, "exed.pid");
1660
- SPAWN_LOCK_PATH = path8.join(EXE_AI_DIR, "exed-spawn.lock");
1739
+ init_daemon_auth();
1740
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path9.join(EXE_AI_DIR, "exed.sock");
1741
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path9.join(EXE_AI_DIR, "exed.pid");
1742
+ SPAWN_LOCK_PATH = path9.join(EXE_AI_DIR, "exed-spawn.lock");
1661
1743
  SPAWN_LOCK_STALE_MS = 3e4;
1662
1744
  CONNECT_TIMEOUT_MS = 15e3;
1663
1745
  REQUEST_TIMEOUT_MS = 3e4;
1746
+ DAEMON_TOKEN_ENV = "EXE_DAEMON_TOKEN";
1664
1747
  _socket = null;
1665
1748
  _connected = false;
1666
1749
  _buffer = "";
@@ -2239,6 +2322,7 @@ async function ensureSchema() {
2239
2322
  project TEXT NOT NULL,
2240
2323
  summary TEXT NOT NULL,
2241
2324
  task_file TEXT,
2325
+ session_scope TEXT,
2242
2326
  read INTEGER NOT NULL DEFAULT 0,
2243
2327
  created_at TEXT NOT NULL
2244
2328
  );
@@ -2247,7 +2331,7 @@ async function ensureSchema() {
2247
2331
  ON notifications(read);
2248
2332
 
2249
2333
  CREATE INDEX IF NOT EXISTS idx_notifications_agent
2250
- ON notifications(agent_id);
2334
+ ON notifications(agent_id, session_scope);
2251
2335
 
2252
2336
  CREATE INDEX IF NOT EXISTS idx_notifications_task_file
2253
2337
  ON notifications(task_file);
@@ -2285,6 +2369,7 @@ async function ensureSchema() {
2285
2369
  target_agent TEXT NOT NULL,
2286
2370
  target_project TEXT,
2287
2371
  target_device TEXT NOT NULL DEFAULT 'local',
2372
+ session_scope TEXT,
2288
2373
  content TEXT NOT NULL,
2289
2374
  priority TEXT DEFAULT 'normal',
2290
2375
  status TEXT DEFAULT 'pending',
@@ -2298,10 +2383,31 @@ async function ensureSchema() {
2298
2383
  );
2299
2384
 
2300
2385
  CREATE INDEX IF NOT EXISTS idx_messages_target
2301
- ON messages(target_agent, status);
2386
+ ON messages(target_agent, session_scope, status);
2302
2387
 
2303
2388
  CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
2304
- ON messages(target_agent, from_agent, server_seq);
2389
+ ON messages(target_agent, session_scope, from_agent, server_seq);
2390
+ `);
2391
+ try {
2392
+ await client.execute({
2393
+ sql: `ALTER TABLE notifications ADD COLUMN session_scope TEXT`,
2394
+ args: []
2395
+ });
2396
+ } catch {
2397
+ }
2398
+ try {
2399
+ await client.execute({
2400
+ sql: `ALTER TABLE messages ADD COLUMN session_scope TEXT`,
2401
+ args: []
2402
+ });
2403
+ } catch {
2404
+ }
2405
+ await client.executeMultiple(`
2406
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent_scope_read
2407
+ ON notifications(agent_id, session_scope, read, created_at);
2408
+
2409
+ CREATE INDEX IF NOT EXISTS idx_messages_target_scope_status
2410
+ ON messages(target_agent, session_scope, status, created_at);
2305
2411
  `);
2306
2412
  try {
2307
2413
  await client.execute({
@@ -2885,6 +2991,13 @@ async function ensureSchema() {
2885
2991
  } catch {
2886
2992
  }
2887
2993
  }
2994
+ try {
2995
+ await client.execute({
2996
+ sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
2997
+ args: []
2998
+ });
2999
+ } catch {
3000
+ }
2888
3001
  }
2889
3002
  async function disposeDatabase() {
2890
3003
  if (_walCheckpointTimer) {
@@ -2923,18 +3036,21 @@ var init_database = __esm({
2923
3036
  });
2924
3037
 
2925
3038
  // src/lib/license.ts
2926
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
3039
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
2927
3040
  import { randomUUID as randomUUID3 } from "crypto";
2928
- import path9 from "path";
3041
+ import { createRequire as createRequire2 } from "module";
3042
+ import { pathToFileURL as pathToFileURL2 } from "url";
3043
+ import os8 from "os";
3044
+ import path10 from "path";
2929
3045
  import { jwtVerify, importSPKI } from "jose";
2930
3046
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
2931
3047
  var init_license = __esm({
2932
3048
  "src/lib/license.ts"() {
2933
3049
  "use strict";
2934
3050
  init_config();
2935
- LICENSE_PATH = path9.join(EXE_AI_DIR, "license.key");
2936
- CACHE_PATH = path9.join(EXE_AI_DIR, "license-cache.json");
2937
- DEVICE_ID_PATH = path9.join(EXE_AI_DIR, "device-id");
3051
+ LICENSE_PATH = path10.join(EXE_AI_DIR, "license.key");
3052
+ CACHE_PATH = path10.join(EXE_AI_DIR, "license-cache.json");
3053
+ DEVICE_ID_PATH = path10.join(EXE_AI_DIR, "device-id");
2938
3054
  PLAN_LIMITS = {
2939
3055
  free: { devices: 1, employees: 1, memories: 5e3 },
2940
3056
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -2946,12 +3062,12 @@ var init_license = __esm({
2946
3062
  });
2947
3063
 
2948
3064
  // src/lib/plan-limits.ts
2949
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
2950
- import path10 from "path";
3065
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
3066
+ import path11 from "path";
2951
3067
  function getLicenseSync() {
2952
3068
  try {
2953
- if (!existsSync8(CACHE_PATH2)) return freeLicense();
2954
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
3069
+ if (!existsSync10(CACHE_PATH2)) return freeLicense();
3070
+ const raw = JSON.parse(readFileSync9(CACHE_PATH2, "utf8"));
2955
3071
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
2956
3072
  const parts = raw.token.split(".");
2957
3073
  if (parts.length !== 3) return freeLicense();
@@ -2989,8 +3105,8 @@ function assertEmployeeLimitSync(rosterPath) {
2989
3105
  const filePath = rosterPath ?? EMPLOYEES_PATH;
2990
3106
  let count = 0;
2991
3107
  try {
2992
- if (existsSync8(filePath)) {
2993
- const raw = readFileSync8(filePath, "utf8");
3108
+ if (existsSync10(filePath)) {
3109
+ const raw = readFileSync9(filePath, "utf8");
2994
3110
  const employees = JSON.parse(raw);
2995
3111
  count = Array.isArray(employees) ? employees.length : 0;
2996
3112
  }
@@ -3019,29 +3135,69 @@ var init_plan_limits = __esm({
3019
3135
  this.name = "PlanLimitError";
3020
3136
  }
3021
3137
  };
3022
- CACHE_PATH2 = path10.join(EXE_AI_DIR, "license-cache.json");
3138
+ CACHE_PATH2 = path11.join(EXE_AI_DIR, "license-cache.json");
3139
+ }
3140
+ });
3141
+
3142
+ // src/lib/task-scope.ts
3143
+ var task_scope_exports = {};
3144
+ __export(task_scope_exports, {
3145
+ getCurrentSessionScope: () => getCurrentSessionScope,
3146
+ sessionScopeFilter: () => sessionScopeFilter,
3147
+ strictSessionScopeFilter: () => strictSessionScopeFilter
3148
+ });
3149
+ function getCurrentSessionScope() {
3150
+ try {
3151
+ return resolveExeSession();
3152
+ } catch {
3153
+ return null;
3154
+ }
3155
+ }
3156
+ function sessionScopeFilter(sessionScope, tableAlias) {
3157
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
3158
+ if (!scope) return { sql: "", args: [] };
3159
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
3160
+ return {
3161
+ sql: ` AND (${col} IS NULL OR ${col} = ?)`,
3162
+ args: [scope]
3163
+ };
3164
+ }
3165
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
3166
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
3167
+ if (!scope) return { sql: "", args: [] };
3168
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
3169
+ return {
3170
+ sql: ` AND ${col} = ?`,
3171
+ args: [scope]
3172
+ };
3173
+ }
3174
+ var init_task_scope = __esm({
3175
+ "src/lib/task-scope.ts"() {
3176
+ "use strict";
3177
+ init_tmux_routing();
3023
3178
  }
3024
3179
  });
3025
3180
 
3026
3181
  // src/lib/notifications.ts
3027
- import crypto from "crypto";
3028
- import path11 from "path";
3029
- import os8 from "os";
3182
+ import crypto2 from "crypto";
3183
+ import path12 from "path";
3184
+ import os9 from "os";
3030
3185
  import {
3031
- readFileSync as readFileSync9,
3186
+ readFileSync as readFileSync10,
3032
3187
  readdirSync,
3033
3188
  unlinkSync as unlinkSync3,
3034
- existsSync as existsSync9,
3189
+ existsSync as existsSync11,
3035
3190
  rmdirSync
3036
3191
  } from "fs";
3037
3192
  async function writeNotification(notification) {
3038
3193
  try {
3039
3194
  const client = getClient();
3040
- const id = crypto.randomUUID();
3195
+ const id = crypto2.randomUUID();
3041
3196
  const now = (/* @__PURE__ */ new Date()).toISOString();
3197
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
3042
3198
  await client.execute({
3043
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
3044
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3199
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
3200
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3045
3201
  args: [
3046
3202
  id,
3047
3203
  notification.agentId,
@@ -3050,6 +3206,7 @@ async function writeNotification(notification) {
3050
3206
  notification.project,
3051
3207
  notification.summary,
3052
3208
  notification.taskFile ?? null,
3209
+ sessionScope,
3053
3210
  now
3054
3211
  ]
3055
3212
  });
@@ -3058,12 +3215,14 @@ async function writeNotification(notification) {
3058
3215
  `);
3059
3216
  }
3060
3217
  }
3061
- async function markAsReadByTaskFile(taskFile) {
3218
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
3062
3219
  try {
3063
3220
  const client = getClient();
3221
+ const scope = strictSessionScopeFilter(sessionScope);
3064
3222
  await client.execute({
3065
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
3066
- args: [taskFile]
3223
+ sql: `UPDATE notifications SET read = 1
3224
+ WHERE task_file = ? AND read = 0${scope.sql}`,
3225
+ args: [taskFile, ...scope.args]
3067
3226
  });
3068
3227
  } catch {
3069
3228
  }
@@ -3072,11 +3231,12 @@ var init_notifications = __esm({
3072
3231
  "src/lib/notifications.ts"() {
3073
3232
  "use strict";
3074
3233
  init_database();
3234
+ init_task_scope();
3075
3235
  }
3076
3236
  });
3077
3237
 
3078
3238
  // src/lib/session-kill-telemetry.ts
3079
- import crypto2 from "crypto";
3239
+ import crypto3 from "crypto";
3080
3240
  async function recordSessionKill(input) {
3081
3241
  try {
3082
3242
  const client = getClient();
@@ -3086,7 +3246,7 @@ async function recordSessionKill(input) {
3086
3246
  ticks_idle, estimated_tokens_saved)
3087
3247
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
3088
3248
  args: [
3089
- crypto2.randomUUID(),
3249
+ crypto3.randomUUID(),
3090
3250
  input.sessionName,
3091
3251
  input.agentId,
3092
3252
  (/* @__PURE__ */ new Date()).toISOString(),
@@ -3109,35 +3269,6 @@ var init_session_kill_telemetry = __esm({
3109
3269
  }
3110
3270
  });
3111
3271
 
3112
- // src/lib/task-scope.ts
3113
- var task_scope_exports = {};
3114
- __export(task_scope_exports, {
3115
- getCurrentSessionScope: () => getCurrentSessionScope,
3116
- sessionScopeFilter: () => sessionScopeFilter
3117
- });
3118
- function getCurrentSessionScope() {
3119
- try {
3120
- return resolveExeSession();
3121
- } catch {
3122
- return null;
3123
- }
3124
- }
3125
- function sessionScopeFilter(sessionScope, tableAlias) {
3126
- const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
3127
- if (!scope) return { sql: "", args: [] };
3128
- const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
3129
- return {
3130
- sql: ` AND (${col} IS NULL OR ${col} = ?)`,
3131
- args: [scope]
3132
- };
3133
- }
3134
- var init_task_scope = __esm({
3135
- "src/lib/task-scope.ts"() {
3136
- "use strict";
3137
- init_tmux_routing();
3138
- }
3139
- });
3140
-
3141
3272
  // src/lib/state-bus.ts
3142
3273
  var StateBus, orgBus;
3143
3274
  var init_state_bus = __esm({
@@ -3194,12 +3325,12 @@ var init_state_bus = __esm({
3194
3325
  });
3195
3326
 
3196
3327
  // src/lib/tasks-crud.ts
3197
- import crypto3 from "crypto";
3198
- import path12 from "path";
3199
- import os9 from "os";
3328
+ import crypto4 from "crypto";
3329
+ import path13 from "path";
3330
+ import os10 from "os";
3200
3331
  import { execSync as execSync5 } from "child_process";
3201
3332
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
3202
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
3333
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3203
3334
  async function writeCheckpoint(input) {
3204
3335
  const client = getClient();
3205
3336
  const row = await resolveTask(client, input.taskId);
@@ -3315,7 +3446,7 @@ async function resolveTask(client, identifier, scopeSession) {
3315
3446
  }
3316
3447
  async function createTaskCore(input) {
3317
3448
  const client = getClient();
3318
- const id = crypto3.randomUUID();
3449
+ const id = crypto4.randomUUID();
3319
3450
  const now = (/* @__PURE__ */ new Date()).toISOString();
3320
3451
  const slug = slugify(input.title);
3321
3452
  let earlySessionScope = null;
@@ -3374,8 +3505,8 @@ ${laneWarning}` : laneWarning;
3374
3505
  }
3375
3506
  if (input.baseDir) {
3376
3507
  try {
3377
- await mkdir3(path12.join(input.baseDir, "exe", "output"), { recursive: true });
3378
- await mkdir3(path12.join(input.baseDir, "exe", "research"), { recursive: true });
3508
+ await mkdir3(path13.join(input.baseDir, "exe", "output"), { recursive: true });
3509
+ await mkdir3(path13.join(input.baseDir, "exe", "research"), { recursive: true });
3379
3510
  await ensureArchitectureDoc(input.baseDir, input.projectName);
3380
3511
  await ensureGitignoreExe(input.baseDir);
3381
3512
  } catch {
@@ -3411,13 +3542,19 @@ ${laneWarning}` : laneWarning;
3411
3542
  });
3412
3543
  if (input.baseDir) {
3413
3544
  try {
3414
- const EXE_OS_DIR = path12.join(os9.homedir(), ".exe-os");
3415
- const mdPath = path12.join(EXE_OS_DIR, taskFile);
3416
- const mdDir = path12.dirname(mdPath);
3417
- if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
3545
+ const EXE_OS_DIR = path13.join(os10.homedir(), ".exe-os");
3546
+ const mdPath = path13.join(EXE_OS_DIR, taskFile);
3547
+ const mdDir = path13.dirname(mdPath);
3548
+ if (!existsSync12(mdDir)) await mkdir3(mdDir, { recursive: true });
3418
3549
  const reviewer = input.reviewer ?? input.assignedBy;
3419
3550
  const mdContent = `# ${input.title}
3420
3551
 
3552
+ ## MANDATORY: When done
3553
+
3554
+ You MUST call update_task with status "done" and a result summary when finished.
3555
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3556
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
3557
+
3421
3558
  **ID:** ${id}
3422
3559
  **Status:** ${initialStatus}
3423
3560
  **Priority:** ${input.priority}
@@ -3431,12 +3568,6 @@ ${laneWarning}` : laneWarning;
3431
3568
  ## Context
3432
3569
 
3433
3570
  ${input.context}
3434
-
3435
- ## MANDATORY: When done
3436
-
3437
- You MUST call update_task with status "done" and a result summary when finished.
3438
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3439
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
3440
3571
  `;
3441
3572
  await writeFile3(mdPath, mdContent, "utf-8");
3442
3573
  } catch (err) {
@@ -3685,7 +3816,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
3685
3816
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
3686
3817
  } catch {
3687
3818
  }
3688
- if (input.status === "done" || input.status === "cancelled") {
3819
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
3689
3820
  try {
3690
3821
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
3691
3822
  clearQueueForAgent2(String(row.assigned_to));
@@ -3714,9 +3845,9 @@ async function deleteTaskCore(taskId, _baseDir) {
3714
3845
  return { taskFile, assignedTo, assignedBy, taskSlug };
3715
3846
  }
3716
3847
  async function ensureArchitectureDoc(baseDir, projectName) {
3717
- const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
3848
+ const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
3718
3849
  try {
3719
- if (existsSync10(archPath)) return;
3850
+ if (existsSync12(archPath)) return;
3720
3851
  const template = [
3721
3852
  `# ${projectName} \u2014 System Architecture`,
3722
3853
  "",
@@ -3749,10 +3880,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
3749
3880
  }
3750
3881
  }
3751
3882
  async function ensureGitignoreExe(baseDir) {
3752
- const gitignorePath = path12.join(baseDir, ".gitignore");
3883
+ const gitignorePath = path13.join(baseDir, ".gitignore");
3753
3884
  try {
3754
- if (existsSync10(gitignorePath)) {
3755
- const content = readFileSync10(gitignorePath, "utf-8");
3885
+ if (existsSync12(gitignorePath)) {
3886
+ const content = readFileSync11(gitignorePath, "utf-8");
3756
3887
  if (/^\/?exe\/?$/m.test(content)) return;
3757
3888
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3758
3889
  } else {
@@ -3783,58 +3914,42 @@ var init_tasks_crud = __esm({
3783
3914
  });
3784
3915
 
3785
3916
  // src/lib/tasks-review.ts
3786
- import path13 from "path";
3787
- import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
3917
+ import path14 from "path";
3918
+ import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
3788
3919
  async function countPendingReviews(sessionScope) {
3789
3920
  const client = getClient();
3790
- if (sessionScope) {
3791
- const result2 = await client.execute({
3792
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND session_scope = ?",
3793
- args: [sessionScope]
3794
- });
3795
- return Number(result2.rows[0]?.cnt) || 0;
3796
- }
3921
+ const scope = strictSessionScopeFilter(
3922
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3923
+ );
3797
3924
  const result = await client.execute({
3798
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3799
- args: []
3925
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3926
+ WHERE status = 'needs_review'${scope.sql}`,
3927
+ args: [...scope.args]
3800
3928
  });
3801
3929
  return Number(result.rows[0]?.cnt) || 0;
3802
3930
  }
3803
3931
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3804
3932
  const client = getClient();
3805
- if (sessionScope) {
3806
- const result2 = await client.execute({
3807
- sql: `SELECT COUNT(*) as cnt FROM tasks
3808
- WHERE status = 'needs_review' AND updated_at > ?
3809
- AND session_scope = ?`,
3810
- args: [sinceIso, sessionScope]
3811
- });
3812
- return Number(result2.rows[0]?.cnt) || 0;
3813
- }
3933
+ const scope = strictSessionScopeFilter(
3934
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3935
+ );
3814
3936
  const result = await client.execute({
3815
3937
  sql: `SELECT COUNT(*) as cnt FROM tasks
3816
- WHERE status = 'needs_review' AND updated_at > ?`,
3817
- args: [sinceIso]
3938
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
3939
+ args: [sinceIso, ...scope.args]
3818
3940
  });
3819
3941
  return Number(result.rows[0]?.cnt) || 0;
3820
3942
  }
3821
3943
  async function listPendingReviews(limit, sessionScope) {
3822
3944
  const client = getClient();
3823
- if (sessionScope) {
3824
- const result2 = await client.execute({
3825
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3826
- WHERE status = 'needs_review'
3827
- AND session_scope = ?
3828
- ORDER BY updated_at ASC LIMIT ?`,
3829
- args: [sessionScope, limit]
3830
- });
3831
- return result2.rows;
3832
- }
3945
+ const scope = strictSessionScopeFilter(
3946
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3947
+ );
3833
3948
  const result = await client.execute({
3834
3949
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3835
- WHERE status = 'needs_review'
3950
+ WHERE status = 'needs_review'${scope.sql}
3836
3951
  ORDER BY updated_at ASC LIMIT ?`,
3837
- args: [limit]
3952
+ args: [...scope.args, limit]
3838
3953
  });
3839
3954
  return result.rows;
3840
3955
  }
@@ -3846,7 +3961,7 @@ async function cleanupOrphanedReviews() {
3846
3961
  WHERE status IN ('open', 'needs_review', 'in_progress')
3847
3962
  AND assigned_by = 'system'
3848
3963
  AND title LIKE 'Review:%'
3849
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3964
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
3850
3965
  args: [now]
3851
3966
  });
3852
3967
  const r1b = await client.execute({
@@ -3965,11 +4080,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3965
4080
  );
3966
4081
  }
3967
4082
  try {
3968
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3969
- if (existsSync11(cacheDir)) {
4083
+ const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
4084
+ if (existsSync13(cacheDir)) {
3970
4085
  for (const f of readdirSync2(cacheDir)) {
3971
4086
  if (f.startsWith("review-notified-")) {
3972
- unlinkSync4(path13.join(cacheDir, f));
4087
+ unlinkSync4(path14.join(cacheDir, f));
3973
4088
  }
3974
4089
  }
3975
4090
  }
@@ -3986,11 +4101,12 @@ var init_tasks_review = __esm({
3986
4101
  init_tmux_routing();
3987
4102
  init_session_key();
3988
4103
  init_state_bus();
4104
+ init_task_scope();
3989
4105
  }
3990
4106
  });
3991
4107
 
3992
4108
  // src/lib/tasks-chain.ts
3993
- import path14 from "path";
4109
+ import path15 from "path";
3994
4110
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
3995
4111
  async function cascadeUnblock(taskId, baseDir, now) {
3996
4112
  const client = getClient();
@@ -4007,7 +4123,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
4007
4123
  });
4008
4124
  for (const ur of unblockedRows.rows) {
4009
4125
  try {
4010
- const ubFile = path14.join(baseDir, String(ur.task_file));
4126
+ const ubFile = path15.join(baseDir, String(ur.task_file));
4011
4127
  let ubContent = await readFile3(ubFile, "utf-8");
4012
4128
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
4013
4129
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -4042,7 +4158,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
4042
4158
  const scScope = sessionScopeFilter();
4043
4159
  const remaining = await client.execute({
4044
4160
  sql: `SELECT COUNT(*) as cnt FROM tasks
4045
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
4161
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
4046
4162
  args: [parentTaskId, ...scScope.args]
4047
4163
  });
4048
4164
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -4076,7 +4192,7 @@ var init_tasks_chain = __esm({
4076
4192
 
4077
4193
  // src/lib/project-name.ts
4078
4194
  import { execSync as execSync6 } from "child_process";
4079
- import path15 from "path";
4195
+ import path16 from "path";
4080
4196
  function getProjectName(cwd) {
4081
4197
  const dir = cwd ?? process.cwd();
4082
4198
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -4089,7 +4205,7 @@ function getProjectName(cwd) {
4089
4205
  timeout: 2e3,
4090
4206
  stdio: ["pipe", "pipe", "pipe"]
4091
4207
  }).trim();
4092
- repoRoot = path15.dirname(gitCommonDir);
4208
+ repoRoot = path16.dirname(gitCommonDir);
4093
4209
  } catch {
4094
4210
  repoRoot = execSync6("git rev-parse --show-toplevel", {
4095
4211
  cwd: dir,
@@ -4098,11 +4214,11 @@ function getProjectName(cwd) {
4098
4214
  stdio: ["pipe", "pipe", "pipe"]
4099
4215
  }).trim();
4100
4216
  }
4101
- _cached2 = path15.basename(repoRoot);
4217
+ _cached2 = path16.basename(repoRoot);
4102
4218
  _cachedCwd = dir;
4103
4219
  return _cached2;
4104
4220
  } catch {
4105
- _cached2 = path15.basename(dir);
4221
+ _cached2 = path16.basename(dir);
4106
4222
  _cachedCwd = dir;
4107
4223
  return _cached2;
4108
4224
  }
@@ -4252,10 +4368,10 @@ __export(behaviors_exports, {
4252
4368
  listBehaviorsByDomain: () => listBehaviorsByDomain,
4253
4369
  storeBehavior: () => storeBehavior
4254
4370
  });
4255
- import crypto4 from "crypto";
4371
+ import crypto5 from "crypto";
4256
4372
  async function storeBehavior(opts) {
4257
4373
  const client = getClient();
4258
- const id = crypto4.randomUUID();
4374
+ const id = crypto5.randomUUID();
4259
4375
  const now = (/* @__PURE__ */ new Date()).toISOString();
4260
4376
  await client.execute({
4261
4377
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -4339,7 +4455,7 @@ __export(skill_learning_exports, {
4339
4455
  storeTrajectory: () => storeTrajectory,
4340
4456
  sweepTrajectories: () => sweepTrajectories
4341
4457
  });
4342
- import crypto5 from "crypto";
4458
+ import crypto6 from "crypto";
4343
4459
  async function extractTrajectory(taskId, agentId) {
4344
4460
  const client = getClient();
4345
4461
  const result = await client.execute({
@@ -4368,11 +4484,11 @@ async function extractTrajectory(taskId, agentId) {
4368
4484
  return signature;
4369
4485
  }
4370
4486
  function hashSignature(signature) {
4371
- return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4487
+ return crypto6.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4372
4488
  }
4373
4489
  async function storeTrajectory(opts) {
4374
4490
  const client = getClient();
4375
- const id = crypto5.randomUUID();
4491
+ const id = crypto6.randomUUID();
4376
4492
  const now = (/* @__PURE__ */ new Date()).toISOString();
4377
4493
  const signatureHash = hashSignature(opts.signature);
4378
4494
  await client.execute({
@@ -4637,8 +4753,8 @@ __export(tasks_exports, {
4637
4753
  updateTaskStatus: () => updateTaskStatus,
4638
4754
  writeCheckpoint: () => writeCheckpoint
4639
4755
  });
4640
- import path16 from "path";
4641
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
4756
+ import path17 from "path";
4757
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
4642
4758
  async function createTask(input) {
4643
4759
  const result = await createTaskCore(input);
4644
4760
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -4657,12 +4773,12 @@ async function updateTask(input) {
4657
4773
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
4658
4774
  try {
4659
4775
  const agent = String(row.assigned_to);
4660
- const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
4661
- const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
4776
+ const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
4777
+ const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
4662
4778
  if (input.status === "in_progress") {
4663
4779
  mkdirSync5(cacheDir, { recursive: true });
4664
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4665
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
4780
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4781
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
4666
4782
  try {
4667
4783
  unlinkSync5(cachePath);
4668
4784
  } catch {
@@ -4670,10 +4786,10 @@ async function updateTask(input) {
4670
4786
  }
4671
4787
  } catch {
4672
4788
  }
4673
- if (input.status === "done") {
4789
+ if (input.status === "done" || input.status === "closed") {
4674
4790
  await cleanupReviewFile(row, taskFile, input.baseDir);
4675
4791
  }
4676
- if (input.status === "done" || input.status === "cancelled") {
4792
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
4677
4793
  try {
4678
4794
  const client = getClient();
4679
4795
  const taskTitle = String(row.title);
@@ -4689,7 +4805,7 @@ async function updateTask(input) {
4689
4805
  if (!isCoordinatorName(assignedAgent)) {
4690
4806
  try {
4691
4807
  const draftClient = getClient();
4692
- if (input.status === "done") {
4808
+ if (input.status === "done" || input.status === "closed") {
4693
4809
  await draftClient.execute({
4694
4810
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
4695
4811
  args: [assignedAgent]
@@ -4706,7 +4822,7 @@ async function updateTask(input) {
4706
4822
  try {
4707
4823
  const client = getClient();
4708
4824
  const cascaded = await client.execute({
4709
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
4825
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
4710
4826
  WHERE parent_task_id = ? AND status = 'needs_review'`,
4711
4827
  args: [now, taskId]
4712
4828
  });
@@ -4719,14 +4835,14 @@ async function updateTask(input) {
4719
4835
  } catch {
4720
4836
  }
4721
4837
  }
4722
- const isTerminal = input.status === "done" || input.status === "needs_review";
4838
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
4723
4839
  if (isTerminal) {
4724
4840
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
4725
4841
  if (!isCoordinator) {
4726
4842
  notifyTaskDone();
4727
4843
  }
4728
4844
  await markTaskNotificationsRead(taskFile);
4729
- if (input.status === "done") {
4845
+ if (input.status === "done" || input.status === "closed") {
4730
4846
  try {
4731
4847
  await cascadeUnblock(taskId, input.baseDir, now);
4732
4848
  } catch {
@@ -4746,7 +4862,7 @@ async function updateTask(input) {
4746
4862
  }
4747
4863
  }
4748
4864
  }
4749
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4865
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4750
4866
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4751
4867
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4752
4868
  taskId,
@@ -5118,6 +5234,7 @@ __export(tmux_routing_exports, {
5118
5234
  isEmployeeAlive: () => isEmployeeAlive,
5119
5235
  isExeSession: () => isExeSession,
5120
5236
  isSessionBusy: () => isSessionBusy,
5237
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
5121
5238
  notifyParentExe: () => notifyParentExe,
5122
5239
  parseParentExe: () => parseParentExe,
5123
5240
  registerParentExe: () => registerParentExe,
@@ -5128,13 +5245,13 @@ __export(tmux_routing_exports, {
5128
5245
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
5129
5246
  });
5130
5247
  import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
5131
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync, readdirSync as readdirSync3 } from "fs";
5132
- import path17 from "path";
5133
- import os10 from "os";
5248
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, existsSync as existsSync14, appendFileSync, readdirSync as readdirSync3 } from "fs";
5249
+ import path18 from "path";
5250
+ import os11 from "os";
5134
5251
  import { fileURLToPath as fileURLToPath2 } from "url";
5135
5252
  import { unlinkSync as unlinkSync6 } from "fs";
5136
5253
  function spawnLockPath(sessionName) {
5137
- return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5254
+ return path18.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5138
5255
  }
5139
5256
  function isProcessAlive(pid) {
5140
5257
  try {
@@ -5145,13 +5262,13 @@ function isProcessAlive(pid) {
5145
5262
  }
5146
5263
  }
5147
5264
  function acquireSpawnLock2(sessionName) {
5148
- if (!existsSync12(SPAWN_LOCK_DIR)) {
5265
+ if (!existsSync14(SPAWN_LOCK_DIR)) {
5149
5266
  mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
5150
5267
  }
5151
5268
  const lockFile = spawnLockPath(sessionName);
5152
- if (existsSync12(lockFile)) {
5269
+ if (existsSync14(lockFile)) {
5153
5270
  try {
5154
- const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
5271
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
5155
5272
  const age = Date.now() - lock.timestamp;
5156
5273
  if (isProcessAlive(lock.pid) && age < 6e4) {
5157
5274
  return false;
@@ -5159,7 +5276,7 @@ function acquireSpawnLock2(sessionName) {
5159
5276
  } catch {
5160
5277
  }
5161
5278
  }
5162
- writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5279
+ writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5163
5280
  return true;
5164
5281
  }
5165
5282
  function releaseSpawnLock2(sessionName) {
@@ -5171,13 +5288,13 @@ function releaseSpawnLock2(sessionName) {
5171
5288
  function resolveBehaviorsExporterScript() {
5172
5289
  try {
5173
5290
  const thisFile = fileURLToPath2(import.meta.url);
5174
- const scriptPath = path17.join(
5175
- path17.dirname(thisFile),
5291
+ const scriptPath = path18.join(
5292
+ path18.dirname(thisFile),
5176
5293
  "..",
5177
5294
  "bin",
5178
5295
  "exe-export-behaviors.js"
5179
5296
  );
5180
- return existsSync12(scriptPath) ? scriptPath : null;
5297
+ return existsSync14(scriptPath) ? scriptPath : null;
5181
5298
  } catch {
5182
5299
  return null;
5183
5300
  }
@@ -5243,12 +5360,12 @@ function extractRootExe(name) {
5243
5360
  return parts.length > 0 ? parts[parts.length - 1] : null;
5244
5361
  }
5245
5362
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5246
- if (!existsSync12(SESSION_CACHE)) {
5363
+ if (!existsSync14(SESSION_CACHE)) {
5247
5364
  mkdirSync6(SESSION_CACHE, { recursive: true });
5248
5365
  }
5249
5366
  const rootExe = extractRootExe(parentExe) ?? parentExe;
5250
- const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5251
- writeFileSync7(filePath, JSON.stringify({
5367
+ const filePath = path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5368
+ writeFileSync8(filePath, JSON.stringify({
5252
5369
  parentExe: rootExe,
5253
5370
  dispatchedBy: dispatchedBy || rootExe,
5254
5371
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5256,7 +5373,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5256
5373
  }
5257
5374
  function getParentExe(sessionKey) {
5258
5375
  try {
5259
- const data = JSON.parse(readFileSync11(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5376
+ const data = JSON.parse(readFileSync12(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5260
5377
  return data.parentExe || null;
5261
5378
  } catch {
5262
5379
  return null;
@@ -5264,8 +5381,8 @@ function getParentExe(sessionKey) {
5264
5381
  }
5265
5382
  function getDispatchedBy(sessionKey) {
5266
5383
  try {
5267
- const data = JSON.parse(readFileSync11(
5268
- path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5384
+ const data = JSON.parse(readFileSync12(
5385
+ path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5269
5386
  "utf8"
5270
5387
  ));
5271
5388
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -5335,8 +5452,8 @@ async function verifyPaneAtCapacity(sessionName) {
5335
5452
  }
5336
5453
  function readDebounceState() {
5337
5454
  try {
5338
- if (!existsSync12(DEBOUNCE_FILE)) return {};
5339
- const raw = JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
5455
+ if (!existsSync14(DEBOUNCE_FILE)) return {};
5456
+ const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
5340
5457
  const state = {};
5341
5458
  for (const [key, val] of Object.entries(raw)) {
5342
5459
  if (typeof val === "number") {
@@ -5352,8 +5469,8 @@ function readDebounceState() {
5352
5469
  }
5353
5470
  function writeDebounceState(state) {
5354
5471
  try {
5355
- if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
5356
- writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
5472
+ if (!existsSync14(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
5473
+ writeFileSync8(DEBOUNCE_FILE, JSON.stringify(state));
5357
5474
  } catch {
5358
5475
  }
5359
5476
  }
@@ -5451,8 +5568,8 @@ function sendIntercom(targetSession) {
5451
5568
  try {
5452
5569
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5453
5570
  const agent = baseAgentName(rawAgent);
5454
- const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5455
- if (existsSync12(markerPath)) {
5571
+ const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
5572
+ if (existsSync14(markerPath)) {
5456
5573
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5457
5574
  return "debounced";
5458
5575
  }
@@ -5461,8 +5578,8 @@ function sendIntercom(targetSession) {
5461
5578
  try {
5462
5579
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5463
5580
  const agent = baseAgentName(rawAgent);
5464
- const taskDir = path17.join(process.cwd(), "exe", agent);
5465
- if (existsSync12(taskDir)) {
5581
+ const taskDir = path18.join(process.cwd(), "exe", agent);
5582
+ if (existsSync14(taskDir)) {
5466
5583
  const files = readdirSync3(taskDir).filter(
5467
5584
  (f) => f.endsWith(".md") && f !== "DONE.txt"
5468
5585
  );
@@ -5522,6 +5639,21 @@ function notifyParentExe(sessionKey) {
5522
5639
  }
5523
5640
  return true;
5524
5641
  }
5642
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
5643
+ const transport = getTransport();
5644
+ try {
5645
+ const sessions = transport.listSessions();
5646
+ if (!sessions.includes(coordinatorSession)) return false;
5647
+ execSync7(
5648
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5649
+ { timeout: 3e3 }
5650
+ );
5651
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
5652
+ return true;
5653
+ } catch {
5654
+ return false;
5655
+ }
5656
+ }
5525
5657
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
5526
5658
  if (isCoordinatorName(employeeName)) {
5527
5659
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -5595,26 +5727,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5595
5727
  const transport = getTransport();
5596
5728
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
5597
5729
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
5598
- const logDir = path17.join(os10.homedir(), ".exe-os", "session-logs");
5599
- const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5600
- if (!existsSync12(logDir)) {
5730
+ const logDir = path18.join(os11.homedir(), ".exe-os", "session-logs");
5731
+ const logFile = path18.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5732
+ if (!existsSync14(logDir)) {
5601
5733
  mkdirSync6(logDir, { recursive: true });
5602
5734
  }
5603
5735
  transport.kill(sessionName);
5604
5736
  let cleanupSuffix = "";
5605
5737
  try {
5606
5738
  const thisFile = fileURLToPath2(import.meta.url);
5607
- const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5608
- if (existsSync12(cleanupScript)) {
5739
+ const cleanupScript = path18.join(path18.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5740
+ if (existsSync14(cleanupScript)) {
5609
5741
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
5610
5742
  }
5611
5743
  } catch {
5612
5744
  }
5613
5745
  try {
5614
- const claudeJsonPath = path17.join(os10.homedir(), ".claude.json");
5746
+ const claudeJsonPath = path18.join(os11.homedir(), ".claude.json");
5615
5747
  let claudeJson = {};
5616
5748
  try {
5617
- claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
5749
+ claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
5618
5750
  } catch {
5619
5751
  }
5620
5752
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -5622,17 +5754,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5622
5754
  const trustDir = opts?.cwd ?? projectDir;
5623
5755
  if (!projects[trustDir]) projects[trustDir] = {};
5624
5756
  projects[trustDir].hasTrustDialogAccepted = true;
5625
- writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5757
+ writeFileSync8(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5626
5758
  } catch {
5627
5759
  }
5628
5760
  try {
5629
- const settingsDir = path17.join(os10.homedir(), ".claude", "projects");
5761
+ const settingsDir = path18.join(os11.homedir(), ".claude", "projects");
5630
5762
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
5631
- const projSettingsDir = path17.join(settingsDir, normalizedKey);
5632
- const settingsPath = path17.join(projSettingsDir, "settings.json");
5763
+ const projSettingsDir = path18.join(settingsDir, normalizedKey);
5764
+ const settingsPath = path18.join(projSettingsDir, "settings.json");
5633
5765
  let settings = {};
5634
5766
  try {
5635
- settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
5767
+ settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
5636
5768
  } catch {
5637
5769
  }
5638
5770
  const perms = settings.permissions ?? {};
@@ -5661,7 +5793,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5661
5793
  perms.allow = allow;
5662
5794
  settings.permissions = perms;
5663
5795
  mkdirSync6(projSettingsDir, { recursive: true });
5664
- writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5796
+ writeFileSync8(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5665
5797
  }
5666
5798
  } catch {
5667
5799
  }
@@ -5676,8 +5808,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5676
5808
  let behaviorsFlag = "";
5677
5809
  let legacyFallbackWarned = false;
5678
5810
  if (!useExeAgent && !useBinSymlink) {
5679
- const identityPath = path17.join(
5680
- os10.homedir(),
5811
+ const identityPath = path18.join(
5812
+ os11.homedir(),
5681
5813
  ".exe-os",
5682
5814
  "identity",
5683
5815
  `${employeeName}.md`
@@ -5686,13 +5818,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5686
5818
  const hasAgentFlag = claudeSupportsAgentFlag();
5687
5819
  if (hasAgentFlag) {
5688
5820
  identityFlag = ` --agent ${employeeName}`;
5689
- } else if (existsSync12(identityPath)) {
5821
+ } else if (existsSync14(identityPath)) {
5690
5822
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
5691
5823
  legacyFallbackWarned = true;
5692
5824
  }
5693
5825
  const behaviorsFile = exportBehaviorsSync(
5694
5826
  employeeName,
5695
- path17.basename(spawnCwd),
5827
+ path18.basename(spawnCwd),
5696
5828
  sessionName
5697
5829
  );
5698
5830
  if (behaviorsFile) {
@@ -5707,16 +5839,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5707
5839
  }
5708
5840
  let sessionContextFlag = "";
5709
5841
  try {
5710
- const ctxDir = path17.join(os10.homedir(), ".exe-os", "session-cache");
5842
+ const ctxDir = path18.join(os11.homedir(), ".exe-os", "session-cache");
5711
5843
  mkdirSync6(ctxDir, { recursive: true });
5712
- const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
5844
+ const ctxFile = path18.join(ctxDir, `session-context-${sessionName}.md`);
5713
5845
  const ctxContent = [
5714
5846
  `## Session Context`,
5715
5847
  `You are running in tmux session: ${sessionName}.`,
5716
5848
  `Your parent coordinator session is ${exeSession}.`,
5717
5849
  `Your employees (if any) use the -${exeSession} suffix.`
5718
5850
  ].join("\n");
5719
- writeFileSync7(ctxFile, ctxContent);
5851
+ writeFileSync8(ctxFile, ctxContent);
5720
5852
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5721
5853
  } catch {
5722
5854
  }
@@ -5793,8 +5925,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5793
5925
  transport.pipeLog(sessionName, logFile);
5794
5926
  try {
5795
5927
  const mySession = getMySession();
5796
- const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5797
- writeFileSync7(dispatchInfo, JSON.stringify({
5928
+ const dispatchInfo = path18.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5929
+ writeFileSync8(dispatchInfo, JSON.stringify({
5798
5930
  dispatchedBy: mySession,
5799
5931
  rootExe: exeSession,
5800
5932
  provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
@@ -5868,15 +6000,15 @@ var init_tmux_routing = __esm({
5868
6000
  init_intercom_queue();
5869
6001
  init_plan_limits();
5870
6002
  init_employees();
5871
- SPAWN_LOCK_DIR = path17.join(os10.homedir(), ".exe-os", "spawn-locks");
5872
- SESSION_CACHE = path17.join(os10.homedir(), ".exe-os", "session-cache");
6003
+ SPAWN_LOCK_DIR = path18.join(os11.homedir(), ".exe-os", "spawn-locks");
6004
+ SESSION_CACHE = path18.join(os11.homedir(), ".exe-os", "session-cache");
5873
6005
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5874
6006
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
5875
6007
  VERIFY_PANE_LINES = 200;
5876
6008
  INTERCOM_DEBOUNCE_MS = 3e4;
5877
6009
  CODEX_DEBOUNCE_MS = 12e4;
5878
- INTERCOM_LOG2 = path17.join(os10.homedir(), ".exe-os", "intercom.log");
5879
- DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
6010
+ INTERCOM_LOG2 = path18.join(os11.homedir(), ".exe-os", "intercom.log");
6011
+ DEBOUNCE_FILE = path18.join(SESSION_CACHE, "intercom-debounce.json");
5880
6012
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5881
6013
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
5882
6014
  }
@@ -5893,14 +6025,14 @@ var init_memory = __esm({
5893
6025
 
5894
6026
  // src/lib/keychain.ts
5895
6027
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
5896
- import { existsSync as existsSync13 } from "fs";
5897
- import path18 from "path";
5898
- import os11 from "os";
6028
+ import { existsSync as existsSync15 } from "fs";
6029
+ import path19 from "path";
6030
+ import os12 from "os";
5899
6031
  function getKeyDir() {
5900
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path18.join(os11.homedir(), ".exe-os");
6032
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path19.join(os12.homedir(), ".exe-os");
5901
6033
  }
5902
6034
  function getKeyPath() {
5903
- return path18.join(getKeyDir(), "master.key");
6035
+ return path19.join(getKeyDir(), "master.key");
5904
6036
  }
5905
6037
  async function tryKeytar() {
5906
6038
  try {
@@ -5921,9 +6053,9 @@ async function getMasterKey() {
5921
6053
  }
5922
6054
  }
5923
6055
  const keyPath = getKeyPath();
5924
- if (!existsSync13(keyPath)) {
6056
+ if (!existsSync15(keyPath)) {
5925
6057
  process.stderr.write(
5926
- `[keychain] Key not found at ${keyPath} (HOME=${os11.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
6058
+ `[keychain] Key not found at ${keyPath} (HOME=${os12.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5927
6059
  `
5928
6060
  );
5929
6061
  return null;
@@ -5953,6 +6085,7 @@ var shard_manager_exports = {};
5953
6085
  __export(shard_manager_exports, {
5954
6086
  disposeShards: () => disposeShards,
5955
6087
  ensureShardSchema: () => ensureShardSchema,
6088
+ getOpenShardCount: () => getOpenShardCount,
5956
6089
  getReadyShardClient: () => getReadyShardClient,
5957
6090
  getShardClient: () => getShardClient,
5958
6091
  getShardsDir: () => getShardsDir,
@@ -5961,15 +6094,18 @@ __export(shard_manager_exports, {
5961
6094
  listShards: () => listShards,
5962
6095
  shardExists: () => shardExists
5963
6096
  });
5964
- import path19 from "path";
5965
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
6097
+ import path20 from "path";
6098
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
5966
6099
  import { createClient as createClient2 } from "@libsql/client";
5967
6100
  function initShardManager(encryptionKey) {
5968
6101
  _encryptionKey = encryptionKey;
5969
- if (!existsSync14(SHARDS_DIR)) {
6102
+ if (!existsSync16(SHARDS_DIR)) {
5970
6103
  mkdirSync7(SHARDS_DIR, { recursive: true });
5971
6104
  }
5972
6105
  _shardingEnabled = true;
6106
+ if (_evictionTimer) clearInterval(_evictionTimer);
6107
+ _evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
6108
+ _evictionTimer.unref();
5973
6109
  }
5974
6110
  function isShardingEnabled() {
5975
6111
  return _shardingEnabled;
@@ -5986,21 +6122,28 @@ function getShardClient(projectName) {
5986
6122
  throw new Error(`Invalid project name for shard: "${projectName}"`);
5987
6123
  }
5988
6124
  const cached = _shards.get(safeName);
5989
- if (cached) return cached;
5990
- const dbPath = path19.join(SHARDS_DIR, `${safeName}.db`);
6125
+ if (cached) {
6126
+ _shardLastAccess.set(safeName, Date.now());
6127
+ return cached;
6128
+ }
6129
+ while (_shards.size >= MAX_OPEN_SHARDS) {
6130
+ evictLRU();
6131
+ }
6132
+ const dbPath = path20.join(SHARDS_DIR, `${safeName}.db`);
5991
6133
  const client = createClient2({
5992
6134
  url: `file:${dbPath}`,
5993
6135
  encryptionKey: _encryptionKey
5994
6136
  });
5995
6137
  _shards.set(safeName, client);
6138
+ _shardLastAccess.set(safeName, Date.now());
5996
6139
  return client;
5997
6140
  }
5998
6141
  function shardExists(projectName) {
5999
6142
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
6000
- return existsSync14(path19.join(SHARDS_DIR, `${safeName}.db`));
6143
+ return existsSync16(path20.join(SHARDS_DIR, `${safeName}.db`));
6001
6144
  }
6002
6145
  function listShards() {
6003
- if (!existsSync14(SHARDS_DIR)) return [];
6146
+ if (!existsSync16(SHARDS_DIR)) return [];
6004
6147
  return readdirSync4(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
6005
6148
  }
6006
6149
  async function ensureShardSchema(client) {
@@ -6052,6 +6195,8 @@ async function ensureShardSchema(client) {
6052
6195
  for (const col of [
6053
6196
  "ALTER TABLE memories ADD COLUMN task_id TEXT",
6054
6197
  "ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
6198
+ "ALTER TABLE memories ADD COLUMN author_device_id TEXT",
6199
+ "ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
6055
6200
  "ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
6056
6201
  "ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
6057
6202
  "ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
@@ -6189,21 +6334,69 @@ async function getReadyShardClient(projectName) {
6189
6334
  await ensureShardSchema(client);
6190
6335
  return client;
6191
6336
  }
6337
+ function evictLRU() {
6338
+ let oldest = null;
6339
+ let oldestTime = Infinity;
6340
+ for (const [name, time] of _shardLastAccess) {
6341
+ if (time < oldestTime) {
6342
+ oldestTime = time;
6343
+ oldest = name;
6344
+ }
6345
+ }
6346
+ if (oldest) {
6347
+ const client = _shards.get(oldest);
6348
+ if (client) {
6349
+ client.close();
6350
+ }
6351
+ _shards.delete(oldest);
6352
+ _shardLastAccess.delete(oldest);
6353
+ }
6354
+ }
6355
+ function evictIdleShards() {
6356
+ const now = Date.now();
6357
+ const toEvict = [];
6358
+ for (const [name, lastAccess] of _shardLastAccess) {
6359
+ if (now - lastAccess > SHARD_IDLE_MS) {
6360
+ toEvict.push(name);
6361
+ }
6362
+ }
6363
+ for (const name of toEvict) {
6364
+ const client = _shards.get(name);
6365
+ if (client) {
6366
+ client.close();
6367
+ }
6368
+ _shards.delete(name);
6369
+ _shardLastAccess.delete(name);
6370
+ }
6371
+ }
6372
+ function getOpenShardCount() {
6373
+ return _shards.size;
6374
+ }
6192
6375
  function disposeShards() {
6376
+ if (_evictionTimer) {
6377
+ clearInterval(_evictionTimer);
6378
+ _evictionTimer = null;
6379
+ }
6193
6380
  for (const [, client] of _shards) {
6194
6381
  client.close();
6195
6382
  }
6196
6383
  _shards.clear();
6384
+ _shardLastAccess.clear();
6197
6385
  _shardingEnabled = false;
6198
6386
  _encryptionKey = null;
6199
6387
  }
6200
- var SHARDS_DIR, _shards, _encryptionKey, _shardingEnabled;
6388
+ var SHARDS_DIR, SHARD_IDLE_MS, MAX_OPEN_SHARDS, EVICTION_INTERVAL_MS, _shards, _shardLastAccess, _evictionTimer, _encryptionKey, _shardingEnabled;
6201
6389
  var init_shard_manager = __esm({
6202
6390
  "src/lib/shard-manager.ts"() {
6203
6391
  "use strict";
6204
6392
  init_config();
6205
- SHARDS_DIR = path19.join(EXE_AI_DIR, "shards");
6393
+ SHARDS_DIR = path20.join(EXE_AI_DIR, "shards");
6394
+ SHARD_IDLE_MS = 5 * 60 * 1e3;
6395
+ MAX_OPEN_SHARDS = 10;
6396
+ EVICTION_INTERVAL_MS = 60 * 1e3;
6206
6397
  _shards = /* @__PURE__ */ new Map();
6398
+ _shardLastAccess = /* @__PURE__ */ new Map();
6399
+ _evictionTimer = null;
6207
6400
  _encryptionKey = null;
6208
6401
  _shardingEnabled = false;
6209
6402
  }
@@ -6999,8 +7192,8 @@ function findContainingChunk(filePath, snippet) {
6999
7192
  try {
7000
7193
  const ext = filePath.split(".").pop()?.toLowerCase();
7001
7194
  if (ext !== "ts" && ext !== "tsx" && ext !== "js" && ext !== "jsx") return "";
7002
- const { readFileSync: readFileSync12 } = __require("fs");
7003
- const source = readFileSync12(filePath, "utf8");
7195
+ const { readFileSync: readFileSync13 } = __require("fs");
7196
+ const source = readFileSync13(filePath, "utf8");
7004
7197
  const lines = source.split("\n");
7005
7198
  const lowerSnippet = snippet.toLowerCase().slice(0, 80);
7006
7199
  let matchLine = -1;
@@ -7066,9 +7259,9 @@ function extractBash(input, response) {
7066
7259
  }
7067
7260
  function extractGrep(input, response) {
7068
7261
  const pattern = String(input.pattern ?? "");
7069
- const path20 = input.path ? String(input.path) : "";
7262
+ const path21 = input.path ? String(input.path) : "";
7070
7263
  const output = String(response.text ?? response.content ?? JSON.stringify(response).slice(0, MAX_OUTPUT));
7071
- return `Searched for "${pattern}"${path20 ? ` in ${path20}` : ""}
7264
+ return `Searched for "${pattern}"${path21 ? ` in ${path21}` : ""}
7072
7265
  ${output.slice(0, MAX_OUTPUT)}`;
7073
7266
  }
7074
7267
  function extractGlob(input, response) {
@@ -7171,7 +7364,7 @@ __export(error_detector_exports, {
7171
7364
  errorFingerprint: () => errorFingerprint,
7172
7365
  isExeOsError: () => isExeOsError
7173
7366
  });
7174
- import crypto6 from "crypto";
7367
+ import crypto7 from "crypto";
7175
7368
  function isRealStderr(stderr) {
7176
7369
  const lines = stderr.trim().split("\n");
7177
7370
  const meaningful = lines.filter(
@@ -7242,7 +7435,7 @@ function classifyError(errorText) {
7242
7435
  }
7243
7436
  function errorFingerprint(toolName, errorText) {
7244
7437
  const normalized = errorText.replace(/\d{4}-\d{2}-\d{2}T[\d:.]+Z/g, "TIMESTAMP").replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "UUID").replace(/\/Users\/[^\s]+/g, "PATH").replace(/:\d+:\d+/g, ":LINE:COL").slice(0, 200);
7245
- return crypto6.createHash("sha256").update(`${toolName}:${normalized}`).digest("hex").slice(0, 16);
7438
+ return crypto7.createHash("sha256").update(`${toolName}:${normalized}`).digest("hex").slice(0, 16);
7246
7439
  }
7247
7440
  var ERROR_PATTERNS, FILE_CONTENT_TOOLS, STDERR_IGNORE_PATTERNS, USER_ERROR_PATTERNS, SYSTEM_BUG_PATTERNS;
7248
7441
  var init_error_detector = __esm({