@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
@@ -320,9 +320,47 @@ var init_provider_table = __esm({
320
320
  }
321
321
  });
322
322
 
323
+ // src/lib/secure-files.ts
324
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
325
+ import { chmod, mkdir } from "fs/promises";
326
+ async function ensurePrivateDir(dirPath) {
327
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
328
+ try {
329
+ await chmod(dirPath, PRIVATE_DIR_MODE);
330
+ } catch {
331
+ }
332
+ }
333
+ function ensurePrivateDirSync(dirPath) {
334
+ mkdirSync2(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
335
+ try {
336
+ chmodSync(dirPath, PRIVATE_DIR_MODE);
337
+ } catch {
338
+ }
339
+ }
340
+ async function enforcePrivateFile(filePath) {
341
+ try {
342
+ await chmod(filePath, PRIVATE_FILE_MODE);
343
+ } catch {
344
+ }
345
+ }
346
+ function enforcePrivateFileSync(filePath) {
347
+ try {
348
+ if (existsSync2(filePath)) chmodSync(filePath, PRIVATE_FILE_MODE);
349
+ } catch {
350
+ }
351
+ }
352
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
353
+ var init_secure_files = __esm({
354
+ "src/lib/secure-files.ts"() {
355
+ "use strict";
356
+ PRIVATE_DIR_MODE = 448;
357
+ PRIVATE_FILE_MODE = 384;
358
+ }
359
+ });
360
+
323
361
  // src/lib/config.ts
324
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
325
- import { readFileSync as readFileSync2, existsSync as existsSync2, renameSync } from "fs";
362
+ import { readFile, writeFile } from "fs/promises";
363
+ import { readFileSync as readFileSync2, existsSync as existsSync3, renameSync } from "fs";
326
364
  import path2 from "path";
327
365
  import os2 from "os";
328
366
  function resolveDataDir() {
@@ -330,7 +368,7 @@ function resolveDataDir() {
330
368
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
331
369
  const newDir = path2.join(os2.homedir(), ".exe-os");
332
370
  const legacyDir = path2.join(os2.homedir(), ".exe-mem");
333
- if (!existsSync2(newDir) && existsSync2(legacyDir)) {
371
+ if (!existsSync3(newDir) && existsSync3(legacyDir)) {
334
372
  try {
335
373
  renameSync(legacyDir, newDir);
336
374
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -393,9 +431,9 @@ function normalizeAutoUpdate(raw) {
393
431
  }
394
432
  async function loadConfig() {
395
433
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
396
- await mkdir(dir, { recursive: true });
434
+ await ensurePrivateDir(dir);
397
435
  const configPath = path2.join(dir, "config.json");
398
- if (!existsSync2(configPath)) {
436
+ if (!existsSync3(configPath)) {
399
437
  return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
400
438
  }
401
439
  const raw = await readFile(configPath, "utf-8");
@@ -408,6 +446,7 @@ async function loadConfig() {
408
446
  `);
409
447
  try {
410
448
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
449
+ await enforcePrivateFile(configPath);
411
450
  } catch {
412
451
  }
413
452
  }
@@ -427,6 +466,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
427
466
  var init_config = __esm({
428
467
  "src/lib/config.ts"() {
429
468
  "use strict";
469
+ init_secure_files();
430
470
  EXE_AI_DIR = resolveDataDir();
431
471
  DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
432
472
  MODELS_DIR = path2.join(EXE_AI_DIR, "models");
@@ -531,10 +571,10 @@ var init_runtime_table = __esm({
531
571
  });
532
572
 
533
573
  // src/lib/agent-config.ts
534
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
574
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
535
575
  import path3 from "path";
536
576
  function loadAgentConfig() {
537
- if (!existsSync3(AGENT_CONFIG_PATH)) return {};
577
+ if (!existsSync4(AGENT_CONFIG_PATH)) return {};
538
578
  try {
539
579
  return JSON.parse(readFileSync3(AGENT_CONFIG_PATH, "utf-8"));
540
580
  } catch {
@@ -555,6 +595,7 @@ var init_agent_config = __esm({
555
595
  "use strict";
556
596
  init_config();
557
597
  init_runtime_table();
598
+ init_secure_files();
558
599
  AGENT_CONFIG_PATH = path3.join(EXE_AI_DIR, "agent-config.json");
559
600
  DEFAULT_MODELS = {
560
601
  claude: "claude-opus-4",
@@ -573,16 +614,16 @@ __export(intercom_queue_exports, {
573
614
  queueIntercom: () => queueIntercom,
574
615
  readQueue: () => readQueue
575
616
  });
576
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
617
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
577
618
  import path4 from "path";
578
619
  import os3 from "os";
579
620
  function ensureDir() {
580
621
  const dir = path4.dirname(QUEUE_PATH);
581
- if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
622
+ if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
582
623
  }
583
624
  function readQueue() {
584
625
  try {
585
- if (!existsSync4(QUEUE_PATH)) return [];
626
+ if (!existsSync5(QUEUE_PATH)) return [];
586
627
  return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
587
628
  } catch {
588
629
  return [];
@@ -747,7 +788,7 @@ var init_db_retry = __esm({
747
788
 
748
789
  // src/lib/employees.ts
749
790
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
750
- import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
791
+ import { existsSync as existsSync6, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
751
792
  import { execSync as execSync3 } from "child_process";
752
793
  import path5 from "path";
753
794
  import os4 from "os";
@@ -768,7 +809,7 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
768
809
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
769
810
  }
770
811
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
771
- if (!existsSync5(employeesPath)) return [];
812
+ if (!existsSync6(employeesPath)) return [];
772
813
  try {
773
814
  return JSON.parse(readFileSync5(employeesPath, "utf-8"));
774
815
  } catch {
@@ -1389,13 +1430,50 @@ var init_database_adapter = __esm({
1389
1430
  }
1390
1431
  });
1391
1432
 
1433
+ // src/lib/daemon-auth.ts
1434
+ import crypto from "crypto";
1435
+ import path7 from "path";
1436
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1437
+ function normalizeToken(token) {
1438
+ if (!token) return null;
1439
+ const trimmed = token.trim();
1440
+ return trimmed.length > 0 ? trimmed : null;
1441
+ }
1442
+ function readDaemonToken() {
1443
+ try {
1444
+ if (!existsSync7(DAEMON_TOKEN_PATH)) return null;
1445
+ return normalizeToken(readFileSync6(DAEMON_TOKEN_PATH, "utf8"));
1446
+ } catch {
1447
+ return null;
1448
+ }
1449
+ }
1450
+ function ensureDaemonToken(seed) {
1451
+ const existing = readDaemonToken();
1452
+ if (existing) return existing;
1453
+ const token = normalizeToken(seed) ?? crypto.randomBytes(32).toString("hex");
1454
+ ensurePrivateDirSync(EXE_AI_DIR);
1455
+ writeFileSync5(DAEMON_TOKEN_PATH, `${token}
1456
+ `, "utf8");
1457
+ enforcePrivateFileSync(DAEMON_TOKEN_PATH);
1458
+ return token;
1459
+ }
1460
+ var DAEMON_TOKEN_PATH;
1461
+ var init_daemon_auth = __esm({
1462
+ "src/lib/daemon-auth.ts"() {
1463
+ "use strict";
1464
+ init_config();
1465
+ init_secure_files();
1466
+ DAEMON_TOKEN_PATH = path7.join(EXE_AI_DIR, "exed.token");
1467
+ }
1468
+ });
1469
+
1392
1470
  // src/lib/exe-daemon-client.ts
1393
1471
  import net from "net";
1394
1472
  import os6 from "os";
1395
1473
  import { spawn } from "child_process";
1396
1474
  import { randomUUID } from "crypto";
1397
- import { existsSync as existsSync6, unlinkSync as unlinkSync2, readFileSync as readFileSync6, openSync, closeSync, statSync } from "fs";
1398
- import path7 from "path";
1475
+ import { existsSync as existsSync8, unlinkSync as unlinkSync2, readFileSync as readFileSync7, openSync, closeSync, statSync } from "fs";
1476
+ import path8 from "path";
1399
1477
  import { fileURLToPath as fileURLToPath2 } from "url";
1400
1478
  function handleData(chunk) {
1401
1479
  _buffer += chunk.toString();
@@ -1423,9 +1501,9 @@ function handleData(chunk) {
1423
1501
  }
1424
1502
  }
1425
1503
  function cleanupStaleFiles() {
1426
- if (existsSync6(PID_PATH)) {
1504
+ if (existsSync8(PID_PATH)) {
1427
1505
  try {
1428
- const pid = parseInt(readFileSync6(PID_PATH, "utf8").trim(), 10);
1506
+ const pid = parseInt(readFileSync7(PID_PATH, "utf8").trim(), 10);
1429
1507
  if (pid > 0) {
1430
1508
  try {
1431
1509
  process.kill(pid, 0);
@@ -1446,11 +1524,11 @@ function cleanupStaleFiles() {
1446
1524
  }
1447
1525
  }
1448
1526
  function findPackageRoot() {
1449
- let dir = path7.dirname(fileURLToPath2(import.meta.url));
1450
- const { root } = path7.parse(dir);
1527
+ let dir = path8.dirname(fileURLToPath2(import.meta.url));
1528
+ const { root } = path8.parse(dir);
1451
1529
  while (dir !== root) {
1452
- if (existsSync6(path7.join(dir, "package.json"))) return dir;
1453
- dir = path7.dirname(dir);
1530
+ if (existsSync8(path8.join(dir, "package.json"))) return dir;
1531
+ dir = path8.dirname(dir);
1454
1532
  }
1455
1533
  return null;
1456
1534
  }
@@ -1476,16 +1554,17 @@ function spawnDaemon() {
1476
1554
  process.stderr.write("[exed-client] WARN: cannot find package root\n");
1477
1555
  return;
1478
1556
  }
1479
- const daemonPath = path7.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1480
- if (!existsSync6(daemonPath)) {
1557
+ const daemonPath = path8.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1558
+ if (!existsSync8(daemonPath)) {
1481
1559
  process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
1482
1560
  `);
1483
1561
  return;
1484
1562
  }
1485
1563
  const resolvedPath = daemonPath;
1564
+ const daemonToken = ensureDaemonToken(process.env[DAEMON_TOKEN_ENV] ?? null);
1486
1565
  process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
1487
1566
  `);
1488
- const logPath = path7.join(path7.dirname(SOCKET_PATH), "exed.log");
1567
+ const logPath = path8.join(path8.dirname(SOCKET_PATH), "exed.log");
1489
1568
  let stderrFd = "ignore";
1490
1569
  try {
1491
1570
  stderrFd = openSync(logPath, "a");
@@ -1503,7 +1582,8 @@ function spawnDaemon() {
1503
1582
  TMUX_PANE: void 0,
1504
1583
  // Prevents resolveExeSession() from scoping to one session
1505
1584
  EXE_DAEMON_SOCK: SOCKET_PATH,
1506
- EXE_DAEMON_PID: PID_PATH
1585
+ EXE_DAEMON_PID: PID_PATH,
1586
+ [DAEMON_TOKEN_ENV]: daemonToken
1507
1587
  }
1508
1588
  });
1509
1589
  child.unref();
@@ -1610,13 +1690,14 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1610
1690
  return;
1611
1691
  }
1612
1692
  const id = randomUUID();
1693
+ const token = process.env[DAEMON_TOKEN_ENV] ?? readDaemonToken();
1613
1694
  const timer = setTimeout(() => {
1614
1695
  _pending.delete(id);
1615
1696
  resolve({ error: "Request timeout" });
1616
1697
  }, timeoutMs);
1617
1698
  _pending.set(id, { resolve, timer });
1618
1699
  try {
1619
- _socket.write(JSON.stringify({ id, ...payload }) + "\n");
1700
+ _socket.write(JSON.stringify({ id, token, ...payload }) + "\n");
1620
1701
  } catch {
1621
1702
  clearTimeout(timer);
1622
1703
  _pending.delete(id);
@@ -1627,17 +1708,19 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1627
1708
  function isClientConnected() {
1628
1709
  return _connected;
1629
1710
  }
1630
- var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
1711
+ 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;
1631
1712
  var init_exe_daemon_client = __esm({
1632
1713
  "src/lib/exe-daemon-client.ts"() {
1633
1714
  "use strict";
1634
1715
  init_config();
1635
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path7.join(EXE_AI_DIR, "exed.sock");
1636
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path7.join(EXE_AI_DIR, "exed.pid");
1637
- SPAWN_LOCK_PATH = path7.join(EXE_AI_DIR, "exed-spawn.lock");
1716
+ init_daemon_auth();
1717
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path8.join(EXE_AI_DIR, "exed.sock");
1718
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path8.join(EXE_AI_DIR, "exed.pid");
1719
+ SPAWN_LOCK_PATH = path8.join(EXE_AI_DIR, "exed-spawn.lock");
1638
1720
  SPAWN_LOCK_STALE_MS = 3e4;
1639
1721
  CONNECT_TIMEOUT_MS = 15e3;
1640
1722
  REQUEST_TIMEOUT_MS = 3e4;
1723
+ DAEMON_TOKEN_ENV = "EXE_DAEMON_TOKEN";
1641
1724
  _socket = null;
1642
1725
  _connected = false;
1643
1726
  _buffer = "";
@@ -2216,6 +2299,7 @@ async function ensureSchema() {
2216
2299
  project TEXT NOT NULL,
2217
2300
  summary TEXT NOT NULL,
2218
2301
  task_file TEXT,
2302
+ session_scope TEXT,
2219
2303
  read INTEGER NOT NULL DEFAULT 0,
2220
2304
  created_at TEXT NOT NULL
2221
2305
  );
@@ -2224,7 +2308,7 @@ async function ensureSchema() {
2224
2308
  ON notifications(read);
2225
2309
 
2226
2310
  CREATE INDEX IF NOT EXISTS idx_notifications_agent
2227
- ON notifications(agent_id);
2311
+ ON notifications(agent_id, session_scope);
2228
2312
 
2229
2313
  CREATE INDEX IF NOT EXISTS idx_notifications_task_file
2230
2314
  ON notifications(task_file);
@@ -2262,6 +2346,7 @@ async function ensureSchema() {
2262
2346
  target_agent TEXT NOT NULL,
2263
2347
  target_project TEXT,
2264
2348
  target_device TEXT NOT NULL DEFAULT 'local',
2349
+ session_scope TEXT,
2265
2350
  content TEXT NOT NULL,
2266
2351
  priority TEXT DEFAULT 'normal',
2267
2352
  status TEXT DEFAULT 'pending',
@@ -2275,10 +2360,31 @@ async function ensureSchema() {
2275
2360
  );
2276
2361
 
2277
2362
  CREATE INDEX IF NOT EXISTS idx_messages_target
2278
- ON messages(target_agent, status);
2363
+ ON messages(target_agent, session_scope, status);
2279
2364
 
2280
2365
  CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
2281
- ON messages(target_agent, from_agent, server_seq);
2366
+ ON messages(target_agent, session_scope, from_agent, server_seq);
2367
+ `);
2368
+ try {
2369
+ await client.execute({
2370
+ sql: `ALTER TABLE notifications ADD COLUMN session_scope TEXT`,
2371
+ args: []
2372
+ });
2373
+ } catch {
2374
+ }
2375
+ try {
2376
+ await client.execute({
2377
+ sql: `ALTER TABLE messages ADD COLUMN session_scope TEXT`,
2378
+ args: []
2379
+ });
2380
+ } catch {
2381
+ }
2382
+ await client.executeMultiple(`
2383
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent_scope_read
2384
+ ON notifications(agent_id, session_scope, read, created_at);
2385
+
2386
+ CREATE INDEX IF NOT EXISTS idx_messages_target_scope_status
2387
+ ON messages(target_agent, session_scope, status, created_at);
2282
2388
  `);
2283
2389
  try {
2284
2390
  await client.execute({
@@ -2862,6 +2968,13 @@ async function ensureSchema() {
2862
2968
  } catch {
2863
2969
  }
2864
2970
  }
2971
+ try {
2972
+ await client.execute({
2973
+ sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
2974
+ args: []
2975
+ });
2976
+ } catch {
2977
+ }
2865
2978
  }
2866
2979
  async function disposeDatabase() {
2867
2980
  if (_walCheckpointTimer) {
@@ -2900,18 +3013,21 @@ var init_database = __esm({
2900
3013
  });
2901
3014
 
2902
3015
  // src/lib/license.ts
2903
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
3016
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
2904
3017
  import { randomUUID as randomUUID2 } from "crypto";
2905
- import path8 from "path";
3018
+ import { createRequire as createRequire2 } from "module";
3019
+ import { pathToFileURL as pathToFileURL2 } from "url";
3020
+ import os7 from "os";
3021
+ import path9 from "path";
2906
3022
  import { jwtVerify, importSPKI } from "jose";
2907
3023
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
2908
3024
  var init_license = __esm({
2909
3025
  "src/lib/license.ts"() {
2910
3026
  "use strict";
2911
3027
  init_config();
2912
- LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
2913
- CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
2914
- DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
3028
+ LICENSE_PATH = path9.join(EXE_AI_DIR, "license.key");
3029
+ CACHE_PATH = path9.join(EXE_AI_DIR, "license-cache.json");
3030
+ DEVICE_ID_PATH = path9.join(EXE_AI_DIR, "device-id");
2915
3031
  PLAN_LIMITS = {
2916
3032
  free: { devices: 1, employees: 1, memories: 5e3 },
2917
3033
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -2923,12 +3039,12 @@ var init_license = __esm({
2923
3039
  });
2924
3040
 
2925
3041
  // src/lib/plan-limits.ts
2926
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
2927
- import path9 from "path";
3042
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
3043
+ import path10 from "path";
2928
3044
  function getLicenseSync() {
2929
3045
  try {
2930
- if (!existsSync8(CACHE_PATH2)) return freeLicense();
2931
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
3046
+ if (!existsSync10(CACHE_PATH2)) return freeLicense();
3047
+ const raw = JSON.parse(readFileSync9(CACHE_PATH2, "utf8"));
2932
3048
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
2933
3049
  const parts = raw.token.split(".");
2934
3050
  if (parts.length !== 3) return freeLicense();
@@ -2966,8 +3082,8 @@ function assertEmployeeLimitSync(rosterPath) {
2966
3082
  const filePath = rosterPath ?? EMPLOYEES_PATH;
2967
3083
  let count = 0;
2968
3084
  try {
2969
- if (existsSync8(filePath)) {
2970
- const raw = readFileSync8(filePath, "utf8");
3085
+ if (existsSync10(filePath)) {
3086
+ const raw = readFileSync9(filePath, "utf8");
2971
3087
  const employees = JSON.parse(raw);
2972
3088
  count = Array.isArray(employees) ? employees.length : 0;
2973
3089
  }
@@ -2996,29 +3112,30 @@ var init_plan_limits = __esm({
2996
3112
  this.name = "PlanLimitError";
2997
3113
  }
2998
3114
  };
2999
- CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
3115
+ CACHE_PATH2 = path10.join(EXE_AI_DIR, "license-cache.json");
3000
3116
  }
3001
3117
  });
3002
3118
 
3003
3119
  // src/lib/notifications.ts
3004
- import crypto from "crypto";
3005
- import path10 from "path";
3006
- import os7 from "os";
3120
+ import crypto2 from "crypto";
3121
+ import path11 from "path";
3122
+ import os8 from "os";
3007
3123
  import {
3008
- readFileSync as readFileSync9,
3124
+ readFileSync as readFileSync10,
3009
3125
  readdirSync,
3010
3126
  unlinkSync as unlinkSync3,
3011
- existsSync as existsSync9,
3127
+ existsSync as existsSync11,
3012
3128
  rmdirSync
3013
3129
  } from "fs";
3014
3130
  async function writeNotification(notification) {
3015
3131
  try {
3016
3132
  const client = getClient();
3017
- const id = crypto.randomUUID();
3133
+ const id = crypto2.randomUUID();
3018
3134
  const now = (/* @__PURE__ */ new Date()).toISOString();
3135
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
3019
3136
  await client.execute({
3020
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
3021
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3137
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
3138
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3022
3139
  args: [
3023
3140
  id,
3024
3141
  notification.agentId,
@@ -3027,6 +3144,7 @@ async function writeNotification(notification) {
3027
3144
  notification.project,
3028
3145
  notification.summary,
3029
3146
  notification.taskFile ?? null,
3147
+ sessionScope,
3030
3148
  now
3031
3149
  ]
3032
3150
  });
@@ -3035,12 +3153,14 @@ async function writeNotification(notification) {
3035
3153
  `);
3036
3154
  }
3037
3155
  }
3038
- async function markAsReadByTaskFile(taskFile) {
3156
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
3039
3157
  try {
3040
3158
  const client = getClient();
3159
+ const scope = strictSessionScopeFilter(sessionScope);
3041
3160
  await client.execute({
3042
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
3043
- args: [taskFile]
3161
+ sql: `UPDATE notifications SET read = 1
3162
+ WHERE task_file = ? AND read = 0${scope.sql}`,
3163
+ args: [taskFile, ...scope.args]
3044
3164
  });
3045
3165
  } catch {
3046
3166
  }
@@ -3049,11 +3169,12 @@ var init_notifications = __esm({
3049
3169
  "src/lib/notifications.ts"() {
3050
3170
  "use strict";
3051
3171
  init_database();
3172
+ init_task_scope();
3052
3173
  }
3053
3174
  });
3054
3175
 
3055
3176
  // src/lib/session-kill-telemetry.ts
3056
- import crypto2 from "crypto";
3177
+ import crypto3 from "crypto";
3057
3178
  async function recordSessionKill(input) {
3058
3179
  try {
3059
3180
  const client = getClient();
@@ -3063,7 +3184,7 @@ async function recordSessionKill(input) {
3063
3184
  ticks_idle, estimated_tokens_saved)
3064
3185
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
3065
3186
  args: [
3066
- crypto2.randomUUID(),
3187
+ crypto3.randomUUID(),
3067
3188
  input.sessionName,
3068
3189
  input.agentId,
3069
3190
  (/* @__PURE__ */ new Date()).toISOString(),
@@ -3142,12 +3263,12 @@ var init_state_bus = __esm({
3142
3263
  });
3143
3264
 
3144
3265
  // src/lib/tasks-crud.ts
3145
- import crypto3 from "crypto";
3146
- import path11 from "path";
3147
- import os8 from "os";
3266
+ import crypto4 from "crypto";
3267
+ import path12 from "path";
3268
+ import os9 from "os";
3148
3269
  import { execSync as execSync4 } from "child_process";
3149
3270
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
3150
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
3271
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3151
3272
  async function writeCheckpoint(input) {
3152
3273
  const client = getClient();
3153
3274
  const row = await resolveTask(client, input.taskId);
@@ -3263,7 +3384,7 @@ async function resolveTask(client, identifier, scopeSession) {
3263
3384
  }
3264
3385
  async function createTaskCore(input) {
3265
3386
  const client = getClient();
3266
- const id = crypto3.randomUUID();
3387
+ const id = crypto4.randomUUID();
3267
3388
  const now = (/* @__PURE__ */ new Date()).toISOString();
3268
3389
  const slug = slugify(input.title);
3269
3390
  let earlySessionScope = null;
@@ -3322,8 +3443,8 @@ ${laneWarning}` : laneWarning;
3322
3443
  }
3323
3444
  if (input.baseDir) {
3324
3445
  try {
3325
- await mkdir3(path11.join(input.baseDir, "exe", "output"), { recursive: true });
3326
- await mkdir3(path11.join(input.baseDir, "exe", "research"), { recursive: true });
3446
+ await mkdir3(path12.join(input.baseDir, "exe", "output"), { recursive: true });
3447
+ await mkdir3(path12.join(input.baseDir, "exe", "research"), { recursive: true });
3327
3448
  await ensureArchitectureDoc(input.baseDir, input.projectName);
3328
3449
  await ensureGitignoreExe(input.baseDir);
3329
3450
  } catch {
@@ -3359,13 +3480,19 @@ ${laneWarning}` : laneWarning;
3359
3480
  });
3360
3481
  if (input.baseDir) {
3361
3482
  try {
3362
- const EXE_OS_DIR = path11.join(os8.homedir(), ".exe-os");
3363
- const mdPath = path11.join(EXE_OS_DIR, taskFile);
3364
- const mdDir = path11.dirname(mdPath);
3365
- if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
3483
+ const EXE_OS_DIR = path12.join(os9.homedir(), ".exe-os");
3484
+ const mdPath = path12.join(EXE_OS_DIR, taskFile);
3485
+ const mdDir = path12.dirname(mdPath);
3486
+ if (!existsSync12(mdDir)) await mkdir3(mdDir, { recursive: true });
3366
3487
  const reviewer = input.reviewer ?? input.assignedBy;
3367
3488
  const mdContent = `# ${input.title}
3368
3489
 
3490
+ ## MANDATORY: When done
3491
+
3492
+ You MUST call update_task with status "done" and a result summary when finished.
3493
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3494
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
3495
+
3369
3496
  **ID:** ${id}
3370
3497
  **Status:** ${initialStatus}
3371
3498
  **Priority:** ${input.priority}
@@ -3379,12 +3506,6 @@ ${laneWarning}` : laneWarning;
3379
3506
  ## Context
3380
3507
 
3381
3508
  ${input.context}
3382
-
3383
- ## MANDATORY: When done
3384
-
3385
- You MUST call update_task with status "done" and a result summary when finished.
3386
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3387
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
3388
3509
  `;
3389
3510
  await writeFile3(mdPath, mdContent, "utf-8");
3390
3511
  } catch (err) {
@@ -3633,7 +3754,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
3633
3754
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
3634
3755
  } catch {
3635
3756
  }
3636
- if (input.status === "done" || input.status === "cancelled") {
3757
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
3637
3758
  try {
3638
3759
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
3639
3760
  clearQueueForAgent2(String(row.assigned_to));
@@ -3662,9 +3783,9 @@ async function deleteTaskCore(taskId, _baseDir) {
3662
3783
  return { taskFile, assignedTo, assignedBy, taskSlug };
3663
3784
  }
3664
3785
  async function ensureArchitectureDoc(baseDir, projectName) {
3665
- const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
3786
+ const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
3666
3787
  try {
3667
- if (existsSync10(archPath)) return;
3788
+ if (existsSync12(archPath)) return;
3668
3789
  const template = [
3669
3790
  `# ${projectName} \u2014 System Architecture`,
3670
3791
  "",
@@ -3697,10 +3818,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
3697
3818
  }
3698
3819
  }
3699
3820
  async function ensureGitignoreExe(baseDir) {
3700
- const gitignorePath = path11.join(baseDir, ".gitignore");
3821
+ const gitignorePath = path12.join(baseDir, ".gitignore");
3701
3822
  try {
3702
- if (existsSync10(gitignorePath)) {
3703
- const content = readFileSync10(gitignorePath, "utf-8");
3823
+ if (existsSync12(gitignorePath)) {
3824
+ const content = readFileSync11(gitignorePath, "utf-8");
3704
3825
  if (/^\/?exe\/?$/m.test(content)) return;
3705
3826
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3706
3827
  } else {
@@ -3731,58 +3852,42 @@ var init_tasks_crud = __esm({
3731
3852
  });
3732
3853
 
3733
3854
  // src/lib/tasks-review.ts
3734
- import path12 from "path";
3735
- import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
3855
+ import path13 from "path";
3856
+ import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
3736
3857
  async function countPendingReviews(sessionScope) {
3737
3858
  const client = getClient();
3738
- if (sessionScope) {
3739
- const result2 = await client.execute({
3740
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND session_scope = ?",
3741
- args: [sessionScope]
3742
- });
3743
- return Number(result2.rows[0]?.cnt) || 0;
3744
- }
3859
+ const scope = strictSessionScopeFilter(
3860
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3861
+ );
3745
3862
  const result = await client.execute({
3746
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3747
- args: []
3863
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3864
+ WHERE status = 'needs_review'${scope.sql}`,
3865
+ args: [...scope.args]
3748
3866
  });
3749
3867
  return Number(result.rows[0]?.cnt) || 0;
3750
3868
  }
3751
3869
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3752
3870
  const client = getClient();
3753
- if (sessionScope) {
3754
- const result2 = await client.execute({
3755
- sql: `SELECT COUNT(*) as cnt FROM tasks
3756
- WHERE status = 'needs_review' AND updated_at > ?
3757
- AND session_scope = ?`,
3758
- args: [sinceIso, sessionScope]
3759
- });
3760
- return Number(result2.rows[0]?.cnt) || 0;
3761
- }
3871
+ const scope = strictSessionScopeFilter(
3872
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3873
+ );
3762
3874
  const result = await client.execute({
3763
3875
  sql: `SELECT COUNT(*) as cnt FROM tasks
3764
- WHERE status = 'needs_review' AND updated_at > ?`,
3765
- args: [sinceIso]
3876
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
3877
+ args: [sinceIso, ...scope.args]
3766
3878
  });
3767
3879
  return Number(result.rows[0]?.cnt) || 0;
3768
3880
  }
3769
3881
  async function listPendingReviews(limit, sessionScope) {
3770
3882
  const client = getClient();
3771
- if (sessionScope) {
3772
- const result2 = await client.execute({
3773
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3774
- WHERE status = 'needs_review'
3775
- AND session_scope = ?
3776
- ORDER BY updated_at ASC LIMIT ?`,
3777
- args: [sessionScope, limit]
3778
- });
3779
- return result2.rows;
3780
- }
3883
+ const scope = strictSessionScopeFilter(
3884
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3885
+ );
3781
3886
  const result = await client.execute({
3782
3887
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3783
- WHERE status = 'needs_review'
3888
+ WHERE status = 'needs_review'${scope.sql}
3784
3889
  ORDER BY updated_at ASC LIMIT ?`,
3785
- args: [limit]
3890
+ args: [...scope.args, limit]
3786
3891
  });
3787
3892
  return result.rows;
3788
3893
  }
@@ -3794,7 +3899,7 @@ async function cleanupOrphanedReviews() {
3794
3899
  WHERE status IN ('open', 'needs_review', 'in_progress')
3795
3900
  AND assigned_by = 'system'
3796
3901
  AND title LIKE 'Review:%'
3797
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3902
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
3798
3903
  args: [now]
3799
3904
  });
3800
3905
  const r1b = await client.execute({
@@ -3913,11 +4018,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3913
4018
  );
3914
4019
  }
3915
4020
  try {
3916
- const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3917
- if (existsSync11(cacheDir)) {
4021
+ const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
4022
+ if (existsSync13(cacheDir)) {
3918
4023
  for (const f of readdirSync2(cacheDir)) {
3919
4024
  if (f.startsWith("review-notified-")) {
3920
- unlinkSync4(path12.join(cacheDir, f));
4025
+ unlinkSync4(path13.join(cacheDir, f));
3921
4026
  }
3922
4027
  }
3923
4028
  }
@@ -3934,11 +4039,12 @@ var init_tasks_review = __esm({
3934
4039
  init_tmux_routing();
3935
4040
  init_session_key();
3936
4041
  init_state_bus();
4042
+ init_task_scope();
3937
4043
  }
3938
4044
  });
3939
4045
 
3940
4046
  // src/lib/tasks-chain.ts
3941
- import path13 from "path";
4047
+ import path14 from "path";
3942
4048
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
3943
4049
  async function cascadeUnblock(taskId, baseDir, now) {
3944
4050
  const client = getClient();
@@ -3955,7 +4061,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
3955
4061
  });
3956
4062
  for (const ur of unblockedRows.rows) {
3957
4063
  try {
3958
- const ubFile = path13.join(baseDir, String(ur.task_file));
4064
+ const ubFile = path14.join(baseDir, String(ur.task_file));
3959
4065
  let ubContent = await readFile3(ubFile, "utf-8");
3960
4066
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
3961
4067
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -3990,7 +4096,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
3990
4096
  const scScope = sessionScopeFilter();
3991
4097
  const remaining = await client.execute({
3992
4098
  sql: `SELECT COUNT(*) as cnt FROM tasks
3993
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
4099
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
3994
4100
  args: [parentTaskId, ...scScope.args]
3995
4101
  });
3996
4102
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -4029,7 +4135,7 @@ __export(project_name_exports, {
4029
4135
  getProjectName: () => getProjectName
4030
4136
  });
4031
4137
  import { execSync as execSync5 } from "child_process";
4032
- import path14 from "path";
4138
+ import path15 from "path";
4033
4139
  function getProjectName(cwd) {
4034
4140
  const dir = cwd ?? process.cwd();
4035
4141
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -4042,7 +4148,7 @@ function getProjectName(cwd) {
4042
4148
  timeout: 2e3,
4043
4149
  stdio: ["pipe", "pipe", "pipe"]
4044
4150
  }).trim();
4045
- repoRoot = path14.dirname(gitCommonDir);
4151
+ repoRoot = path15.dirname(gitCommonDir);
4046
4152
  } catch {
4047
4153
  repoRoot = execSync5("git rev-parse --show-toplevel", {
4048
4154
  cwd: dir,
@@ -4051,11 +4157,11 @@ function getProjectName(cwd) {
4051
4157
  stdio: ["pipe", "pipe", "pipe"]
4052
4158
  }).trim();
4053
4159
  }
4054
- _cached2 = path14.basename(repoRoot);
4160
+ _cached2 = path15.basename(repoRoot);
4055
4161
  _cachedCwd = dir;
4056
4162
  return _cached2;
4057
4163
  } catch {
4058
- _cached2 = path14.basename(dir);
4164
+ _cached2 = path15.basename(dir);
4059
4165
  _cachedCwd = dir;
4060
4166
  return _cached2;
4061
4167
  }
@@ -4202,10 +4308,10 @@ var init_tasks_notify = __esm({
4202
4308
  });
4203
4309
 
4204
4310
  // src/lib/behaviors.ts
4205
- import crypto4 from "crypto";
4311
+ import crypto5 from "crypto";
4206
4312
  async function storeBehavior(opts) {
4207
4313
  const client = getClient();
4208
- const id = crypto4.randomUUID();
4314
+ const id = crypto5.randomUUID();
4209
4315
  const now = (/* @__PURE__ */ new Date()).toISOString();
4210
4316
  await client.execute({
4211
4317
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -4234,7 +4340,7 @@ __export(skill_learning_exports, {
4234
4340
  storeTrajectory: () => storeTrajectory,
4235
4341
  sweepTrajectories: () => sweepTrajectories
4236
4342
  });
4237
- import crypto5 from "crypto";
4343
+ import crypto6 from "crypto";
4238
4344
  async function extractTrajectory(taskId, agentId) {
4239
4345
  const client = getClient();
4240
4346
  const result = await client.execute({
@@ -4263,11 +4369,11 @@ async function extractTrajectory(taskId, agentId) {
4263
4369
  return signature;
4264
4370
  }
4265
4371
  function hashSignature(signature) {
4266
- return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4372
+ return crypto6.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4267
4373
  }
4268
4374
  async function storeTrajectory(opts) {
4269
4375
  const client = getClient();
4270
- const id = crypto5.randomUUID();
4376
+ const id = crypto6.randomUUID();
4271
4377
  const now = (/* @__PURE__ */ new Date()).toISOString();
4272
4378
  const signatureHash = hashSignature(opts.signature);
4273
4379
  await client.execute({
@@ -4532,8 +4638,8 @@ __export(tasks_exports, {
4532
4638
  updateTaskStatus: () => updateTaskStatus,
4533
4639
  writeCheckpoint: () => writeCheckpoint
4534
4640
  });
4535
- import path15 from "path";
4536
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
4641
+ import path16 from "path";
4642
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
4537
4643
  async function createTask(input) {
4538
4644
  const result = await createTaskCore(input);
4539
4645
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -4552,12 +4658,12 @@ async function updateTask(input) {
4552
4658
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
4553
4659
  try {
4554
4660
  const agent = String(row.assigned_to);
4555
- const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
4556
- const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
4661
+ const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
4662
+ const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
4557
4663
  if (input.status === "in_progress") {
4558
4664
  mkdirSync5(cacheDir, { recursive: true });
4559
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4560
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
4665
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4666
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
4561
4667
  try {
4562
4668
  unlinkSync5(cachePath);
4563
4669
  } catch {
@@ -4565,10 +4671,10 @@ async function updateTask(input) {
4565
4671
  }
4566
4672
  } catch {
4567
4673
  }
4568
- if (input.status === "done") {
4674
+ if (input.status === "done" || input.status === "closed") {
4569
4675
  await cleanupReviewFile(row, taskFile, input.baseDir);
4570
4676
  }
4571
- if (input.status === "done" || input.status === "cancelled") {
4677
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
4572
4678
  try {
4573
4679
  const client = getClient();
4574
4680
  const taskTitle = String(row.title);
@@ -4584,7 +4690,7 @@ async function updateTask(input) {
4584
4690
  if (!isCoordinatorName(assignedAgent)) {
4585
4691
  try {
4586
4692
  const draftClient = getClient();
4587
- if (input.status === "done") {
4693
+ if (input.status === "done" || input.status === "closed") {
4588
4694
  await draftClient.execute({
4589
4695
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
4590
4696
  args: [assignedAgent]
@@ -4601,7 +4707,7 @@ async function updateTask(input) {
4601
4707
  try {
4602
4708
  const client = getClient();
4603
4709
  const cascaded = await client.execute({
4604
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
4710
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
4605
4711
  WHERE parent_task_id = ? AND status = 'needs_review'`,
4606
4712
  args: [now, taskId]
4607
4713
  });
@@ -4614,14 +4720,14 @@ async function updateTask(input) {
4614
4720
  } catch {
4615
4721
  }
4616
4722
  }
4617
- const isTerminal = input.status === "done" || input.status === "needs_review";
4723
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
4618
4724
  if (isTerminal) {
4619
4725
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
4620
4726
  if (!isCoordinator) {
4621
4727
  notifyTaskDone();
4622
4728
  }
4623
4729
  await markTaskNotificationsRead(taskFile);
4624
- if (input.status === "done") {
4730
+ if (input.status === "done" || input.status === "closed") {
4625
4731
  try {
4626
4732
  await cascadeUnblock(taskId, input.baseDir, now);
4627
4733
  } catch {
@@ -4641,7 +4747,7 @@ async function updateTask(input) {
4641
4747
  }
4642
4748
  }
4643
4749
  }
4644
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4750
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4645
4751
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4646
4752
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4647
4753
  taskId,
@@ -5013,6 +5119,7 @@ __export(tmux_routing_exports, {
5013
5119
  isEmployeeAlive: () => isEmployeeAlive,
5014
5120
  isExeSession: () => isExeSession,
5015
5121
  isSessionBusy: () => isSessionBusy,
5122
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
5016
5123
  notifyParentExe: () => notifyParentExe,
5017
5124
  parseParentExe: () => parseParentExe,
5018
5125
  registerParentExe: () => registerParentExe,
@@ -5023,13 +5130,13 @@ __export(tmux_routing_exports, {
5023
5130
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
5024
5131
  });
5025
5132
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
5026
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync, readdirSync as readdirSync3 } from "fs";
5027
- import path16 from "path";
5028
- import os9 from "os";
5133
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, existsSync as existsSync14, appendFileSync, readdirSync as readdirSync3 } from "fs";
5134
+ import path17 from "path";
5135
+ import os10 from "os";
5029
5136
  import { fileURLToPath as fileURLToPath3 } from "url";
5030
5137
  import { unlinkSync as unlinkSync6 } from "fs";
5031
5138
  function spawnLockPath(sessionName) {
5032
- return path16.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5139
+ return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5033
5140
  }
5034
5141
  function isProcessAlive(pid) {
5035
5142
  try {
@@ -5040,13 +5147,13 @@ function isProcessAlive(pid) {
5040
5147
  }
5041
5148
  }
5042
5149
  function acquireSpawnLock2(sessionName) {
5043
- if (!existsSync12(SPAWN_LOCK_DIR)) {
5150
+ if (!existsSync14(SPAWN_LOCK_DIR)) {
5044
5151
  mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
5045
5152
  }
5046
5153
  const lockFile = spawnLockPath(sessionName);
5047
- if (existsSync12(lockFile)) {
5154
+ if (existsSync14(lockFile)) {
5048
5155
  try {
5049
- const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
5156
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
5050
5157
  const age = Date.now() - lock.timestamp;
5051
5158
  if (isProcessAlive(lock.pid) && age < 6e4) {
5052
5159
  return false;
@@ -5054,7 +5161,7 @@ function acquireSpawnLock2(sessionName) {
5054
5161
  } catch {
5055
5162
  }
5056
5163
  }
5057
- writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5164
+ writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5058
5165
  return true;
5059
5166
  }
5060
5167
  function releaseSpawnLock2(sessionName) {
@@ -5066,13 +5173,13 @@ function releaseSpawnLock2(sessionName) {
5066
5173
  function resolveBehaviorsExporterScript() {
5067
5174
  try {
5068
5175
  const thisFile = fileURLToPath3(import.meta.url);
5069
- const scriptPath = path16.join(
5070
- path16.dirname(thisFile),
5176
+ const scriptPath = path17.join(
5177
+ path17.dirname(thisFile),
5071
5178
  "..",
5072
5179
  "bin",
5073
5180
  "exe-export-behaviors.js"
5074
5181
  );
5075
- return existsSync12(scriptPath) ? scriptPath : null;
5182
+ return existsSync14(scriptPath) ? scriptPath : null;
5076
5183
  } catch {
5077
5184
  return null;
5078
5185
  }
@@ -5138,12 +5245,12 @@ function extractRootExe(name) {
5138
5245
  return parts.length > 0 ? parts[parts.length - 1] : null;
5139
5246
  }
5140
5247
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5141
- if (!existsSync12(SESSION_CACHE)) {
5248
+ if (!existsSync14(SESSION_CACHE)) {
5142
5249
  mkdirSync6(SESSION_CACHE, { recursive: true });
5143
5250
  }
5144
5251
  const rootExe = extractRootExe(parentExe) ?? parentExe;
5145
- const filePath = path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5146
- writeFileSync7(filePath, JSON.stringify({
5252
+ const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5253
+ writeFileSync8(filePath, JSON.stringify({
5147
5254
  parentExe: rootExe,
5148
5255
  dispatchedBy: dispatchedBy || rootExe,
5149
5256
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5151,7 +5258,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5151
5258
  }
5152
5259
  function getParentExe(sessionKey) {
5153
5260
  try {
5154
- const data = JSON.parse(readFileSync11(path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5261
+ const data = JSON.parse(readFileSync12(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5155
5262
  return data.parentExe || null;
5156
5263
  } catch {
5157
5264
  return null;
@@ -5159,8 +5266,8 @@ function getParentExe(sessionKey) {
5159
5266
  }
5160
5267
  function getDispatchedBy(sessionKey) {
5161
5268
  try {
5162
- const data = JSON.parse(readFileSync11(
5163
- path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5269
+ const data = JSON.parse(readFileSync12(
5270
+ path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5164
5271
  "utf8"
5165
5272
  ));
5166
5273
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -5230,8 +5337,8 @@ async function verifyPaneAtCapacity(sessionName) {
5230
5337
  }
5231
5338
  function readDebounceState() {
5232
5339
  try {
5233
- if (!existsSync12(DEBOUNCE_FILE)) return {};
5234
- const raw = JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
5340
+ if (!existsSync14(DEBOUNCE_FILE)) return {};
5341
+ const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
5235
5342
  const state = {};
5236
5343
  for (const [key, val] of Object.entries(raw)) {
5237
5344
  if (typeof val === "number") {
@@ -5247,8 +5354,8 @@ function readDebounceState() {
5247
5354
  }
5248
5355
  function writeDebounceState(state) {
5249
5356
  try {
5250
- if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
5251
- writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
5357
+ if (!existsSync14(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
5358
+ writeFileSync8(DEBOUNCE_FILE, JSON.stringify(state));
5252
5359
  } catch {
5253
5360
  }
5254
5361
  }
@@ -5346,8 +5453,8 @@ function sendIntercom(targetSession) {
5346
5453
  try {
5347
5454
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5348
5455
  const agent = baseAgentName(rawAgent);
5349
- const markerPath = path16.join(SESSION_CACHE, `current-task-${agent}.json`);
5350
- if (existsSync12(markerPath)) {
5456
+ const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5457
+ if (existsSync14(markerPath)) {
5351
5458
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5352
5459
  return "debounced";
5353
5460
  }
@@ -5356,8 +5463,8 @@ function sendIntercom(targetSession) {
5356
5463
  try {
5357
5464
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5358
5465
  const agent = baseAgentName(rawAgent);
5359
- const taskDir = path16.join(process.cwd(), "exe", agent);
5360
- if (existsSync12(taskDir)) {
5466
+ const taskDir = path17.join(process.cwd(), "exe", agent);
5467
+ if (existsSync14(taskDir)) {
5361
5468
  const files = readdirSync3(taskDir).filter(
5362
5469
  (f) => f.endsWith(".md") && f !== "DONE.txt"
5363
5470
  );
@@ -5417,6 +5524,21 @@ function notifyParentExe(sessionKey) {
5417
5524
  }
5418
5525
  return true;
5419
5526
  }
5527
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
5528
+ const transport = getTransport();
5529
+ try {
5530
+ const sessions = transport.listSessions();
5531
+ if (!sessions.includes(coordinatorSession)) return false;
5532
+ execSync6(
5533
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5534
+ { timeout: 3e3 }
5535
+ );
5536
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
5537
+ return true;
5538
+ } catch {
5539
+ return false;
5540
+ }
5541
+ }
5420
5542
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
5421
5543
  if (isCoordinatorName(employeeName)) {
5422
5544
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -5490,26 +5612,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5490
5612
  const transport = getTransport();
5491
5613
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
5492
5614
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
5493
- const logDir = path16.join(os9.homedir(), ".exe-os", "session-logs");
5494
- const logFile = path16.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5495
- if (!existsSync12(logDir)) {
5615
+ const logDir = path17.join(os10.homedir(), ".exe-os", "session-logs");
5616
+ const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5617
+ if (!existsSync14(logDir)) {
5496
5618
  mkdirSync6(logDir, { recursive: true });
5497
5619
  }
5498
5620
  transport.kill(sessionName);
5499
5621
  let cleanupSuffix = "";
5500
5622
  try {
5501
5623
  const thisFile = fileURLToPath3(import.meta.url);
5502
- const cleanupScript = path16.join(path16.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5503
- if (existsSync12(cleanupScript)) {
5624
+ const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5625
+ if (existsSync14(cleanupScript)) {
5504
5626
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
5505
5627
  }
5506
5628
  } catch {
5507
5629
  }
5508
5630
  try {
5509
- const claudeJsonPath = path16.join(os9.homedir(), ".claude.json");
5631
+ const claudeJsonPath = path17.join(os10.homedir(), ".claude.json");
5510
5632
  let claudeJson = {};
5511
5633
  try {
5512
- claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
5634
+ claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
5513
5635
  } catch {
5514
5636
  }
5515
5637
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -5517,17 +5639,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5517
5639
  const trustDir = opts?.cwd ?? projectDir;
5518
5640
  if (!projects[trustDir]) projects[trustDir] = {};
5519
5641
  projects[trustDir].hasTrustDialogAccepted = true;
5520
- writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5642
+ writeFileSync8(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5521
5643
  } catch {
5522
5644
  }
5523
5645
  try {
5524
- const settingsDir = path16.join(os9.homedir(), ".claude", "projects");
5646
+ const settingsDir = path17.join(os10.homedir(), ".claude", "projects");
5525
5647
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
5526
- const projSettingsDir = path16.join(settingsDir, normalizedKey);
5527
- const settingsPath = path16.join(projSettingsDir, "settings.json");
5648
+ const projSettingsDir = path17.join(settingsDir, normalizedKey);
5649
+ const settingsPath = path17.join(projSettingsDir, "settings.json");
5528
5650
  let settings = {};
5529
5651
  try {
5530
- settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
5652
+ settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
5531
5653
  } catch {
5532
5654
  }
5533
5655
  const perms = settings.permissions ?? {};
@@ -5556,7 +5678,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5556
5678
  perms.allow = allow;
5557
5679
  settings.permissions = perms;
5558
5680
  mkdirSync6(projSettingsDir, { recursive: true });
5559
- writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5681
+ writeFileSync8(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5560
5682
  }
5561
5683
  } catch {
5562
5684
  }
@@ -5571,8 +5693,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5571
5693
  let behaviorsFlag = "";
5572
5694
  let legacyFallbackWarned = false;
5573
5695
  if (!useExeAgent && !useBinSymlink) {
5574
- const identityPath = path16.join(
5575
- os9.homedir(),
5696
+ const identityPath = path17.join(
5697
+ os10.homedir(),
5576
5698
  ".exe-os",
5577
5699
  "identity",
5578
5700
  `${employeeName}.md`
@@ -5581,13 +5703,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5581
5703
  const hasAgentFlag = claudeSupportsAgentFlag();
5582
5704
  if (hasAgentFlag) {
5583
5705
  identityFlag = ` --agent ${employeeName}`;
5584
- } else if (existsSync12(identityPath)) {
5706
+ } else if (existsSync14(identityPath)) {
5585
5707
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
5586
5708
  legacyFallbackWarned = true;
5587
5709
  }
5588
5710
  const behaviorsFile = exportBehaviorsSync(
5589
5711
  employeeName,
5590
- path16.basename(spawnCwd),
5712
+ path17.basename(spawnCwd),
5591
5713
  sessionName
5592
5714
  );
5593
5715
  if (behaviorsFile) {
@@ -5602,16 +5724,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5602
5724
  }
5603
5725
  let sessionContextFlag = "";
5604
5726
  try {
5605
- const ctxDir = path16.join(os9.homedir(), ".exe-os", "session-cache");
5727
+ const ctxDir = path17.join(os10.homedir(), ".exe-os", "session-cache");
5606
5728
  mkdirSync6(ctxDir, { recursive: true });
5607
- const ctxFile = path16.join(ctxDir, `session-context-${sessionName}.md`);
5729
+ const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
5608
5730
  const ctxContent = [
5609
5731
  `## Session Context`,
5610
5732
  `You are running in tmux session: ${sessionName}.`,
5611
5733
  `Your parent coordinator session is ${exeSession}.`,
5612
5734
  `Your employees (if any) use the -${exeSession} suffix.`
5613
5735
  ].join("\n");
5614
- writeFileSync7(ctxFile, ctxContent);
5736
+ writeFileSync8(ctxFile, ctxContent);
5615
5737
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5616
5738
  } catch {
5617
5739
  }
@@ -5688,8 +5810,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5688
5810
  transport.pipeLog(sessionName, logFile);
5689
5811
  try {
5690
5812
  const mySession = getMySession();
5691
- const dispatchInfo = path16.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5692
- writeFileSync7(dispatchInfo, JSON.stringify({
5813
+ const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5814
+ writeFileSync8(dispatchInfo, JSON.stringify({
5693
5815
  dispatchedBy: mySession,
5694
5816
  rootExe: exeSession,
5695
5817
  provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
@@ -5763,15 +5885,15 @@ var init_tmux_routing = __esm({
5763
5885
  init_intercom_queue();
5764
5886
  init_plan_limits();
5765
5887
  init_employees();
5766
- SPAWN_LOCK_DIR = path16.join(os9.homedir(), ".exe-os", "spawn-locks");
5767
- SESSION_CACHE = path16.join(os9.homedir(), ".exe-os", "session-cache");
5888
+ SPAWN_LOCK_DIR = path17.join(os10.homedir(), ".exe-os", "spawn-locks");
5889
+ SESSION_CACHE = path17.join(os10.homedir(), ".exe-os", "session-cache");
5768
5890
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5769
5891
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
5770
5892
  VERIFY_PANE_LINES = 200;
5771
5893
  INTERCOM_DEBOUNCE_MS = 3e4;
5772
5894
  CODEX_DEBOUNCE_MS = 12e4;
5773
- INTERCOM_LOG2 = path16.join(os9.homedir(), ".exe-os", "intercom.log");
5774
- DEBOUNCE_FILE = path16.join(SESSION_CACHE, "intercom-debounce.json");
5895
+ INTERCOM_LOG2 = path17.join(os10.homedir(), ".exe-os", "intercom.log");
5896
+ DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
5775
5897
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5776
5898
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
5777
5899
  }
@@ -5794,6 +5916,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
5794
5916
  args: [scope]
5795
5917
  };
5796
5918
  }
5919
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
5920
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
5921
+ if (!scope) return { sql: "", args: [] };
5922
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
5923
+ return {
5924
+ sql: ` AND ${col} = ?`,
5925
+ args: [scope]
5926
+ };
5927
+ }
5797
5928
  var init_task_scope = __esm({
5798
5929
  "src/lib/task-scope.ts"() {
5799
5930
  "use strict";
@@ -5812,14 +5943,14 @@ var init_memory = __esm({
5812
5943
 
5813
5944
  // src/lib/keychain.ts
5814
5945
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
5815
- import { existsSync as existsSync13 } from "fs";
5816
- import path17 from "path";
5817
- import os10 from "os";
5946
+ import { existsSync as existsSync15 } from "fs";
5947
+ import path18 from "path";
5948
+ import os11 from "os";
5818
5949
  function getKeyDir() {
5819
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path17.join(os10.homedir(), ".exe-os");
5950
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path18.join(os11.homedir(), ".exe-os");
5820
5951
  }
5821
5952
  function getKeyPath() {
5822
- return path17.join(getKeyDir(), "master.key");
5953
+ return path18.join(getKeyDir(), "master.key");
5823
5954
  }
5824
5955
  async function tryKeytar() {
5825
5956
  try {
@@ -5840,9 +5971,9 @@ async function getMasterKey() {
5840
5971
  }
5841
5972
  }
5842
5973
  const keyPath = getKeyPath();
5843
- if (!existsSync13(keyPath)) {
5974
+ if (!existsSync15(keyPath)) {
5844
5975
  process.stderr.write(
5845
- `[keychain] Key not found at ${keyPath} (HOME=${os10.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5976
+ `[keychain] Key not found at ${keyPath} (HOME=${os11.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5846
5977
  `
5847
5978
  );
5848
5979
  return null;
@@ -5872,6 +6003,7 @@ var shard_manager_exports = {};
5872
6003
  __export(shard_manager_exports, {
5873
6004
  disposeShards: () => disposeShards,
5874
6005
  ensureShardSchema: () => ensureShardSchema,
6006
+ getOpenShardCount: () => getOpenShardCount,
5875
6007
  getReadyShardClient: () => getReadyShardClient,
5876
6008
  getShardClient: () => getShardClient,
5877
6009
  getShardsDir: () => getShardsDir,
@@ -5880,15 +6012,18 @@ __export(shard_manager_exports, {
5880
6012
  listShards: () => listShards,
5881
6013
  shardExists: () => shardExists
5882
6014
  });
5883
- import path18 from "path";
5884
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
6015
+ import path19 from "path";
6016
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
5885
6017
  import { createClient as createClient2 } from "@libsql/client";
5886
6018
  function initShardManager(encryptionKey) {
5887
6019
  _encryptionKey = encryptionKey;
5888
- if (!existsSync14(SHARDS_DIR)) {
6020
+ if (!existsSync16(SHARDS_DIR)) {
5889
6021
  mkdirSync7(SHARDS_DIR, { recursive: true });
5890
6022
  }
5891
6023
  _shardingEnabled = true;
6024
+ if (_evictionTimer) clearInterval(_evictionTimer);
6025
+ _evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
6026
+ _evictionTimer.unref();
5892
6027
  }
5893
6028
  function isShardingEnabled() {
5894
6029
  return _shardingEnabled;
@@ -5905,21 +6040,28 @@ function getShardClient(projectName) {
5905
6040
  throw new Error(`Invalid project name for shard: "${projectName}"`);
5906
6041
  }
5907
6042
  const cached = _shards.get(safeName);
5908
- if (cached) return cached;
5909
- const dbPath = path18.join(SHARDS_DIR, `${safeName}.db`);
6043
+ if (cached) {
6044
+ _shardLastAccess.set(safeName, Date.now());
6045
+ return cached;
6046
+ }
6047
+ while (_shards.size >= MAX_OPEN_SHARDS) {
6048
+ evictLRU();
6049
+ }
6050
+ const dbPath = path19.join(SHARDS_DIR, `${safeName}.db`);
5910
6051
  const client = createClient2({
5911
6052
  url: `file:${dbPath}`,
5912
6053
  encryptionKey: _encryptionKey
5913
6054
  });
5914
6055
  _shards.set(safeName, client);
6056
+ _shardLastAccess.set(safeName, Date.now());
5915
6057
  return client;
5916
6058
  }
5917
6059
  function shardExists(projectName) {
5918
6060
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
5919
- return existsSync14(path18.join(SHARDS_DIR, `${safeName}.db`));
6061
+ return existsSync16(path19.join(SHARDS_DIR, `${safeName}.db`));
5920
6062
  }
5921
6063
  function listShards() {
5922
- if (!existsSync14(SHARDS_DIR)) return [];
6064
+ if (!existsSync16(SHARDS_DIR)) return [];
5923
6065
  return readdirSync4(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
5924
6066
  }
5925
6067
  async function ensureShardSchema(client) {
@@ -5971,6 +6113,8 @@ async function ensureShardSchema(client) {
5971
6113
  for (const col of [
5972
6114
  "ALTER TABLE memories ADD COLUMN task_id TEXT",
5973
6115
  "ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
6116
+ "ALTER TABLE memories ADD COLUMN author_device_id TEXT",
6117
+ "ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
5974
6118
  "ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
5975
6119
  "ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
5976
6120
  "ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
@@ -6108,21 +6252,69 @@ async function getReadyShardClient(projectName) {
6108
6252
  await ensureShardSchema(client);
6109
6253
  return client;
6110
6254
  }
6255
+ function evictLRU() {
6256
+ let oldest = null;
6257
+ let oldestTime = Infinity;
6258
+ for (const [name, time] of _shardLastAccess) {
6259
+ if (time < oldestTime) {
6260
+ oldestTime = time;
6261
+ oldest = name;
6262
+ }
6263
+ }
6264
+ if (oldest) {
6265
+ const client = _shards.get(oldest);
6266
+ if (client) {
6267
+ client.close();
6268
+ }
6269
+ _shards.delete(oldest);
6270
+ _shardLastAccess.delete(oldest);
6271
+ }
6272
+ }
6273
+ function evictIdleShards() {
6274
+ const now = Date.now();
6275
+ const toEvict = [];
6276
+ for (const [name, lastAccess] of _shardLastAccess) {
6277
+ if (now - lastAccess > SHARD_IDLE_MS) {
6278
+ toEvict.push(name);
6279
+ }
6280
+ }
6281
+ for (const name of toEvict) {
6282
+ const client = _shards.get(name);
6283
+ if (client) {
6284
+ client.close();
6285
+ }
6286
+ _shards.delete(name);
6287
+ _shardLastAccess.delete(name);
6288
+ }
6289
+ }
6290
+ function getOpenShardCount() {
6291
+ return _shards.size;
6292
+ }
6111
6293
  function disposeShards() {
6294
+ if (_evictionTimer) {
6295
+ clearInterval(_evictionTimer);
6296
+ _evictionTimer = null;
6297
+ }
6112
6298
  for (const [, client] of _shards) {
6113
6299
  client.close();
6114
6300
  }
6115
6301
  _shards.clear();
6302
+ _shardLastAccess.clear();
6116
6303
  _shardingEnabled = false;
6117
6304
  _encryptionKey = null;
6118
6305
  }
6119
- var SHARDS_DIR, _shards, _encryptionKey, _shardingEnabled;
6306
+ var SHARDS_DIR, SHARD_IDLE_MS, MAX_OPEN_SHARDS, EVICTION_INTERVAL_MS, _shards, _shardLastAccess, _evictionTimer, _encryptionKey, _shardingEnabled;
6120
6307
  var init_shard_manager = __esm({
6121
6308
  "src/lib/shard-manager.ts"() {
6122
6309
  "use strict";
6123
6310
  init_config();
6124
- SHARDS_DIR = path18.join(EXE_AI_DIR, "shards");
6311
+ SHARDS_DIR = path19.join(EXE_AI_DIR, "shards");
6312
+ SHARD_IDLE_MS = 5 * 60 * 1e3;
6313
+ MAX_OPEN_SHARDS = 10;
6314
+ EVICTION_INTERVAL_MS = 60 * 1e3;
6125
6315
  _shards = /* @__PURE__ */ new Map();
6316
+ _shardLastAccess = /* @__PURE__ */ new Map();
6317
+ _evictionTimer = null;
6126
6318
  _encryptionKey = null;
6127
6319
  _shardingEnabled = false;
6128
6320
  }
@@ -6886,9 +7078,9 @@ var init_store = __esm({
6886
7078
  });
6887
7079
 
6888
7080
  // src/bin/scan-tasks.ts
6889
- import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
6890
- import path19 from "path";
6891
- import os11 from "os";
7081
+ import { existsSync as existsSync17, readFileSync as readFileSync13 } from "fs";
7082
+ import path20 from "path";
7083
+ import os12 from "os";
6892
7084
 
6893
7085
  // src/lib/is-main.ts
6894
7086
  import { realpathSync } from "fs";
@@ -6908,27 +7100,27 @@ function isMainModule(importMetaUrl) {
6908
7100
  // src/bin/scan-tasks.ts
6909
7101
  init_session_key();
6910
7102
  init_task_scope();
6911
- function getMcpHealthWarning(runtime = getSessionRuntime(), homeDir = os11.homedir()) {
7103
+ function getMcpHealthWarning(runtime = getSessionRuntime(), homeDir = os12.homedir()) {
6912
7104
  if (runtime === "codex") {
6913
7105
  return null;
6914
7106
  }
6915
7107
  try {
6916
7108
  if (runtime === "opencode") {
6917
- const opencodeJson = path19.join(homeDir, ".config", "opencode", "opencode.json");
6918
- if (!existsSync15(opencodeJson)) {
7109
+ const opencodeJson = path20.join(homeDir, ".config", "opencode", "opencode.json");
7110
+ if (!existsSync17(opencodeJson)) {
6919
7111
  return "\u26A0\uFE0F MCP config missing (~/.config/opencode/opencode.json not found) \u2014 exe-os task tools may be unavailable. Run `exe-os opencode`.\n";
6920
7112
  }
6921
- const config2 = JSON.parse(readFileSync12(opencodeJson, "utf8"));
7113
+ const config2 = JSON.parse(readFileSync13(opencodeJson, "utf8"));
6922
7114
  if (!config2.mcp?.["exe-os"]?.enabled) {
6923
7115
  return "\u26A0\uFE0F MCP task tools not available \u2014 exe-os server is not enabled in ~/.config/opencode/opencode.json.\n";
6924
7116
  }
6925
7117
  return null;
6926
7118
  }
6927
- const claudeJson = path19.join(homeDir, ".claude.json");
6928
- if (!existsSync15(claudeJson)) {
7119
+ const claudeJson = path20.join(homeDir, ".claude.json");
7120
+ if (!existsSync17(claudeJson)) {
6929
7121
  return "\u26A0\uFE0F MCP config missing (~/.claude.json not found) \u2014 close_task won't work. Run /exe-setup\n";
6930
7122
  }
6931
- const config = JSON.parse(readFileSync12(claudeJson, "utf8"));
7123
+ const config = JSON.parse(readFileSync13(claudeJson, "utf8"));
6932
7124
  const servers = config.mcpServers;
6933
7125
  if (!servers?.["exe-os"] && !servers?.["exe-mem"]) {
6934
7126
  return "\u26A0\uFE0F MCP task tools not available \u2014 exe-os server not configured in ~/.claude.json. close_task won't work.\n";