@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 path from "path";
32
70
  import os 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 = path.join(os.homedir(), ".exe-os");
37
75
  const legacyDir = path.join(os.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 = path.join(dir, "config.json");
103
- if (!existsSync(configPath)) {
141
+ if (!existsSync2(configPath)) {
104
142
  return { ...DEFAULT_CONFIG, dbPath: path.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 = path.join(EXE_AI_DIR, "memories.db");
137
177
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
@@ -278,7 +318,7 @@ var init_session_key = __esm({
278
318
 
279
319
  // src/lib/employees.ts
280
320
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
281
- import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
321
+ import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
282
322
  import { execSync as execSync2 } from "child_process";
283
323
  import path2 from "path";
284
324
  import os2 from "os";
@@ -302,7 +342,7 @@ function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
302
342
  return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
303
343
  }
304
344
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
305
- if (!existsSync2(employeesPath)) return [];
345
+ if (!existsSync3(employeesPath)) return [];
306
346
  try {
307
347
  return JSON.parse(readFileSync2(employeesPath, "utf-8"));
308
348
  } catch {
@@ -340,13 +380,13 @@ var init_employees = __esm({
340
380
  });
341
381
 
342
382
  // src/lib/session-registry.ts
343
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
383
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
344
384
  import path4 from "path";
345
385
  import os3 from "os";
346
386
  function registerSession(entry) {
347
387
  const dir = path4.dirname(REGISTRY_PATH);
348
- if (!existsSync3(dir)) {
349
- mkdirSync2(dir, { recursive: true });
388
+ if (!existsSync4(dir)) {
389
+ mkdirSync3(dir, { recursive: true });
350
390
  }
351
391
  const sessions = listSessions();
352
392
  const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
@@ -581,10 +621,10 @@ var init_runtime_table = __esm({
581
621
  });
582
622
 
583
623
  // src/lib/agent-config.ts
584
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
624
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
585
625
  import path5 from "path";
586
626
  function loadAgentConfig() {
587
- if (!existsSync4(AGENT_CONFIG_PATH)) return {};
627
+ if (!existsSync5(AGENT_CONFIG_PATH)) return {};
588
628
  try {
589
629
  return JSON.parse(readFileSync5(AGENT_CONFIG_PATH, "utf-8"));
590
630
  } catch {
@@ -605,6 +645,7 @@ var init_agent_config = __esm({
605
645
  "use strict";
606
646
  init_config();
607
647
  init_runtime_table();
648
+ init_secure_files();
608
649
  AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
609
650
  DEFAULT_MODELS = {
610
651
  claude: "claude-opus-4",
@@ -623,16 +664,16 @@ __export(intercom_queue_exports, {
623
664
  queueIntercom: () => queueIntercom,
624
665
  readQueue: () => readQueue
625
666
  });
626
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
667
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
627
668
  import path6 from "path";
628
669
  import os4 from "os";
629
670
  function ensureDir() {
630
671
  const dir = path6.dirname(QUEUE_PATH);
631
- if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
672
+ if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
632
673
  }
633
674
  function readQueue() {
634
675
  try {
635
- if (!existsSync5(QUEUE_PATH)) return [];
676
+ if (!existsSync6(QUEUE_PATH)) return [];
636
677
  return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
637
678
  } catch {
638
679
  return [];
@@ -1379,13 +1420,50 @@ var init_database_adapter = __esm({
1379
1420
  }
1380
1421
  });
1381
1422
 
1423
+ // src/lib/daemon-auth.ts
1424
+ import crypto from "crypto";
1425
+ import path8 from "path";
1426
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
1427
+ function normalizeToken(token) {
1428
+ if (!token) return null;
1429
+ const trimmed = token.trim();
1430
+ return trimmed.length > 0 ? trimmed : null;
1431
+ }
1432
+ function readDaemonToken() {
1433
+ try {
1434
+ if (!existsSync7(DAEMON_TOKEN_PATH)) return null;
1435
+ return normalizeToken(readFileSync7(DAEMON_TOKEN_PATH, "utf8"));
1436
+ } catch {
1437
+ return null;
1438
+ }
1439
+ }
1440
+ function ensureDaemonToken(seed) {
1441
+ const existing = readDaemonToken();
1442
+ if (existing) return existing;
1443
+ const token = normalizeToken(seed) ?? crypto.randomBytes(32).toString("hex");
1444
+ ensurePrivateDirSync(EXE_AI_DIR);
1445
+ writeFileSync6(DAEMON_TOKEN_PATH, `${token}
1446
+ `, "utf8");
1447
+ enforcePrivateFileSync(DAEMON_TOKEN_PATH);
1448
+ return token;
1449
+ }
1450
+ var DAEMON_TOKEN_PATH;
1451
+ var init_daemon_auth = __esm({
1452
+ "src/lib/daemon-auth.ts"() {
1453
+ "use strict";
1454
+ init_config();
1455
+ init_secure_files();
1456
+ DAEMON_TOKEN_PATH = path8.join(EXE_AI_DIR, "exed.token");
1457
+ }
1458
+ });
1459
+
1382
1460
  // src/lib/exe-daemon-client.ts
1383
1461
  import net from "net";
1384
1462
  import os6 from "os";
1385
1463
  import { spawn } from "child_process";
1386
1464
  import { randomUUID } from "crypto";
1387
- import { existsSync as existsSync6, unlinkSync as unlinkSync3, readFileSync as readFileSync7, openSync, closeSync, statSync } from "fs";
1388
- import path8 from "path";
1465
+ import { existsSync as existsSync8, unlinkSync as unlinkSync3, readFileSync as readFileSync8, openSync, closeSync, statSync } from "fs";
1466
+ import path9 from "path";
1389
1467
  import { fileURLToPath } from "url";
1390
1468
  function handleData(chunk) {
1391
1469
  _buffer += chunk.toString();
@@ -1413,9 +1491,9 @@ function handleData(chunk) {
1413
1491
  }
1414
1492
  }
1415
1493
  function cleanupStaleFiles() {
1416
- if (existsSync6(PID_PATH)) {
1494
+ if (existsSync8(PID_PATH)) {
1417
1495
  try {
1418
- const pid = parseInt(readFileSync7(PID_PATH, "utf8").trim(), 10);
1496
+ const pid = parseInt(readFileSync8(PID_PATH, "utf8").trim(), 10);
1419
1497
  if (pid > 0) {
1420
1498
  try {
1421
1499
  process.kill(pid, 0);
@@ -1436,11 +1514,11 @@ function cleanupStaleFiles() {
1436
1514
  }
1437
1515
  }
1438
1516
  function findPackageRoot() {
1439
- let dir = path8.dirname(fileURLToPath(import.meta.url));
1440
- const { root } = path8.parse(dir);
1517
+ let dir = path9.dirname(fileURLToPath(import.meta.url));
1518
+ const { root } = path9.parse(dir);
1441
1519
  while (dir !== root) {
1442
- if (existsSync6(path8.join(dir, "package.json"))) return dir;
1443
- dir = path8.dirname(dir);
1520
+ if (existsSync8(path9.join(dir, "package.json"))) return dir;
1521
+ dir = path9.dirname(dir);
1444
1522
  }
1445
1523
  return null;
1446
1524
  }
@@ -1466,16 +1544,17 @@ function spawnDaemon() {
1466
1544
  process.stderr.write("[exed-client] WARN: cannot find package root\n");
1467
1545
  return;
1468
1546
  }
1469
- const daemonPath = path8.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1470
- if (!existsSync6(daemonPath)) {
1547
+ const daemonPath = path9.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1548
+ if (!existsSync8(daemonPath)) {
1471
1549
  process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
1472
1550
  `);
1473
1551
  return;
1474
1552
  }
1475
1553
  const resolvedPath = daemonPath;
1554
+ const daemonToken = ensureDaemonToken(process.env[DAEMON_TOKEN_ENV] ?? null);
1476
1555
  process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
1477
1556
  `);
1478
- const logPath = path8.join(path8.dirname(SOCKET_PATH), "exed.log");
1557
+ const logPath = path9.join(path9.dirname(SOCKET_PATH), "exed.log");
1479
1558
  let stderrFd = "ignore";
1480
1559
  try {
1481
1560
  stderrFd = openSync(logPath, "a");
@@ -1493,7 +1572,8 @@ function spawnDaemon() {
1493
1572
  TMUX_PANE: void 0,
1494
1573
  // Prevents resolveExeSession() from scoping to one session
1495
1574
  EXE_DAEMON_SOCK: SOCKET_PATH,
1496
- EXE_DAEMON_PID: PID_PATH
1575
+ EXE_DAEMON_PID: PID_PATH,
1576
+ [DAEMON_TOKEN_ENV]: daemonToken
1497
1577
  }
1498
1578
  });
1499
1579
  child.unref();
@@ -1600,13 +1680,14 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1600
1680
  return;
1601
1681
  }
1602
1682
  const id = randomUUID();
1683
+ const token = process.env[DAEMON_TOKEN_ENV] ?? readDaemonToken();
1603
1684
  const timer = setTimeout(() => {
1604
1685
  _pending.delete(id);
1605
1686
  resolve({ error: "Request timeout" });
1606
1687
  }, timeoutMs);
1607
1688
  _pending.set(id, { resolve, timer });
1608
1689
  try {
1609
- _socket.write(JSON.stringify({ id, ...payload }) + "\n");
1690
+ _socket.write(JSON.stringify({ id, token, ...payload }) + "\n");
1610
1691
  } catch {
1611
1692
  clearTimeout(timer);
1612
1693
  _pending.delete(id);
@@ -1617,17 +1698,19 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1617
1698
  function isClientConnected() {
1618
1699
  return _connected;
1619
1700
  }
1620
- var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
1701
+ 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;
1621
1702
  var init_exe_daemon_client = __esm({
1622
1703
  "src/lib/exe-daemon-client.ts"() {
1623
1704
  "use strict";
1624
1705
  init_config();
1625
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path8.join(EXE_AI_DIR, "exed.sock");
1626
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path8.join(EXE_AI_DIR, "exed.pid");
1627
- SPAWN_LOCK_PATH = path8.join(EXE_AI_DIR, "exed-spawn.lock");
1706
+ init_daemon_auth();
1707
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path9.join(EXE_AI_DIR, "exed.sock");
1708
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path9.join(EXE_AI_DIR, "exed.pid");
1709
+ SPAWN_LOCK_PATH = path9.join(EXE_AI_DIR, "exed-spawn.lock");
1628
1710
  SPAWN_LOCK_STALE_MS = 3e4;
1629
1711
  CONNECT_TIMEOUT_MS = 15e3;
1630
1712
  REQUEST_TIMEOUT_MS = 3e4;
1713
+ DAEMON_TOKEN_ENV = "EXE_DAEMON_TOKEN";
1631
1714
  _socket = null;
1632
1715
  _connected = false;
1633
1716
  _buffer = "";
@@ -2206,6 +2289,7 @@ async function ensureSchema() {
2206
2289
  project TEXT NOT NULL,
2207
2290
  summary TEXT NOT NULL,
2208
2291
  task_file TEXT,
2292
+ session_scope TEXT,
2209
2293
  read INTEGER NOT NULL DEFAULT 0,
2210
2294
  created_at TEXT NOT NULL
2211
2295
  );
@@ -2214,7 +2298,7 @@ async function ensureSchema() {
2214
2298
  ON notifications(read);
2215
2299
 
2216
2300
  CREATE INDEX IF NOT EXISTS idx_notifications_agent
2217
- ON notifications(agent_id);
2301
+ ON notifications(agent_id, session_scope);
2218
2302
 
2219
2303
  CREATE INDEX IF NOT EXISTS idx_notifications_task_file
2220
2304
  ON notifications(task_file);
@@ -2252,6 +2336,7 @@ async function ensureSchema() {
2252
2336
  target_agent TEXT NOT NULL,
2253
2337
  target_project TEXT,
2254
2338
  target_device TEXT NOT NULL DEFAULT 'local',
2339
+ session_scope TEXT,
2255
2340
  content TEXT NOT NULL,
2256
2341
  priority TEXT DEFAULT 'normal',
2257
2342
  status TEXT DEFAULT 'pending',
@@ -2265,10 +2350,31 @@ async function ensureSchema() {
2265
2350
  );
2266
2351
 
2267
2352
  CREATE INDEX IF NOT EXISTS idx_messages_target
2268
- ON messages(target_agent, status);
2353
+ ON messages(target_agent, session_scope, status);
2269
2354
 
2270
2355
  CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
2271
- ON messages(target_agent, from_agent, server_seq);
2356
+ ON messages(target_agent, session_scope, from_agent, server_seq);
2357
+ `);
2358
+ try {
2359
+ await client.execute({
2360
+ sql: `ALTER TABLE notifications ADD COLUMN session_scope TEXT`,
2361
+ args: []
2362
+ });
2363
+ } catch {
2364
+ }
2365
+ try {
2366
+ await client.execute({
2367
+ sql: `ALTER TABLE messages ADD COLUMN session_scope TEXT`,
2368
+ args: []
2369
+ });
2370
+ } catch {
2371
+ }
2372
+ await client.executeMultiple(`
2373
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent_scope_read
2374
+ ON notifications(agent_id, session_scope, read, created_at);
2375
+
2376
+ CREATE INDEX IF NOT EXISTS idx_messages_target_scope_status
2377
+ ON messages(target_agent, session_scope, status, created_at);
2272
2378
  `);
2273
2379
  try {
2274
2380
  await client.execute({
@@ -2852,6 +2958,13 @@ async function ensureSchema() {
2852
2958
  } catch {
2853
2959
  }
2854
2960
  }
2961
+ try {
2962
+ await client.execute({
2963
+ sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
2964
+ args: []
2965
+ });
2966
+ } catch {
2967
+ }
2855
2968
  }
2856
2969
  async function disposeDatabase() {
2857
2970
  if (_walCheckpointTimer) {
@@ -2890,18 +3003,21 @@ var init_database = __esm({
2890
3003
  });
2891
3004
 
2892
3005
  // src/lib/license.ts
2893
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
3006
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
2894
3007
  import { randomUUID as randomUUID2 } from "crypto";
2895
- import path9 from "path";
3008
+ import { createRequire as createRequire2 } from "module";
3009
+ import { pathToFileURL as pathToFileURL2 } from "url";
3010
+ import os7 from "os";
3011
+ import path10 from "path";
2896
3012
  import { jwtVerify, importSPKI } from "jose";
2897
3013
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
2898
3014
  var init_license = __esm({
2899
3015
  "src/lib/license.ts"() {
2900
3016
  "use strict";
2901
3017
  init_config();
2902
- LICENSE_PATH = path9.join(EXE_AI_DIR, "license.key");
2903
- CACHE_PATH = path9.join(EXE_AI_DIR, "license-cache.json");
2904
- DEVICE_ID_PATH = path9.join(EXE_AI_DIR, "device-id");
3018
+ LICENSE_PATH = path10.join(EXE_AI_DIR, "license.key");
3019
+ CACHE_PATH = path10.join(EXE_AI_DIR, "license-cache.json");
3020
+ DEVICE_ID_PATH = path10.join(EXE_AI_DIR, "device-id");
2905
3021
  PLAN_LIMITS = {
2906
3022
  free: { devices: 1, employees: 1, memories: 5e3 },
2907
3023
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -2913,12 +3029,12 @@ var init_license = __esm({
2913
3029
  });
2914
3030
 
2915
3031
  // src/lib/plan-limits.ts
2916
- import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
2917
- import path10 from "path";
3032
+ import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
3033
+ import path11 from "path";
2918
3034
  function getLicenseSync() {
2919
3035
  try {
2920
- if (!existsSync8(CACHE_PATH2)) return freeLicense();
2921
- const raw = JSON.parse(readFileSync9(CACHE_PATH2, "utf8"));
3036
+ if (!existsSync10(CACHE_PATH2)) return freeLicense();
3037
+ const raw = JSON.parse(readFileSync10(CACHE_PATH2, "utf8"));
2922
3038
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
2923
3039
  const parts = raw.token.split(".");
2924
3040
  if (parts.length !== 3) return freeLicense();
@@ -2956,8 +3072,8 @@ function assertEmployeeLimitSync(rosterPath) {
2956
3072
  const filePath = rosterPath ?? EMPLOYEES_PATH;
2957
3073
  let count = 0;
2958
3074
  try {
2959
- if (existsSync8(filePath)) {
2960
- const raw = readFileSync9(filePath, "utf8");
3075
+ if (existsSync10(filePath)) {
3076
+ const raw = readFileSync10(filePath, "utf8");
2961
3077
  const employees = JSON.parse(raw);
2962
3078
  count = Array.isArray(employees) ? employees.length : 0;
2963
3079
  }
@@ -2986,7 +3102,7 @@ var init_plan_limits = __esm({
2986
3102
  this.name = "PlanLimitError";
2987
3103
  }
2988
3104
  };
2989
- CACHE_PATH2 = path10.join(EXE_AI_DIR, "license-cache.json");
3105
+ CACHE_PATH2 = path11.join(EXE_AI_DIR, "license-cache.json");
2990
3106
  }
2991
3107
  });
2992
3108
 
@@ -3002,24 +3118,25 @@ __export(notifications_exports, {
3002
3118
  readUnreadNotifications: () => readUnreadNotifications,
3003
3119
  writeNotification: () => writeNotification
3004
3120
  });
3005
- import crypto from "crypto";
3006
- import path11 from "path";
3007
- import os7 from "os";
3121
+ import crypto2 from "crypto";
3122
+ import path12 from "path";
3123
+ import os8 from "os";
3008
3124
  import {
3009
- readFileSync as readFileSync10,
3125
+ readFileSync as readFileSync11,
3010
3126
  readdirSync as readdirSync2,
3011
3127
  unlinkSync as unlinkSync4,
3012
- existsSync as existsSync9,
3128
+ existsSync as existsSync11,
3013
3129
  rmdirSync
3014
3130
  } from "fs";
3015
3131
  async function writeNotification(notification) {
3016
3132
  try {
3017
3133
  const client = getClient();
3018
- const id = crypto.randomUUID();
3134
+ const id = crypto2.randomUUID();
3019
3135
  const now = (/* @__PURE__ */ new Date()).toISOString();
3136
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
3020
3137
  await client.execute({
3021
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
3022
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3138
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
3139
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3023
3140
  args: [
3024
3141
  id,
3025
3142
  notification.agentId,
@@ -3028,6 +3145,7 @@ async function writeNotification(notification) {
3028
3145
  notification.project,
3029
3146
  notification.summary,
3030
3147
  notification.taskFile ?? null,
3148
+ sessionScope,
3031
3149
  now
3032
3150
  ]
3033
3151
  });
@@ -3036,21 +3154,22 @@ async function writeNotification(notification) {
3036
3154
  `);
3037
3155
  }
3038
3156
  }
3039
- async function readUnreadNotifications(agentFilter) {
3157
+ async function readUnreadNotifications(agentFilter, sessionScope) {
3040
3158
  try {
3041
3159
  const client = getClient();
3042
3160
  const conditions = ["read = 0"];
3043
3161
  const args = [];
3162
+ const scope = strictSessionScopeFilter(sessionScope);
3044
3163
  if (agentFilter) {
3045
3164
  conditions.push("agent_id = ?");
3046
3165
  args.push(agentFilter);
3047
3166
  }
3048
3167
  const result = await client.execute({
3049
- sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, created_at
3168
+ sql: `SELECT id, agent_id, agent_role, event, project, summary, task_file, session_scope, created_at
3050
3169
  FROM notifications
3051
- WHERE ${conditions.join(" AND ")}
3170
+ WHERE ${conditions.join(" AND ")}${scope.sql}
3052
3171
  ORDER BY created_at ASC`,
3053
- args
3172
+ args: [...args, ...scope.args]
3054
3173
  });
3055
3174
  return result.rows.map((r) => ({
3056
3175
  id: String(r.id),
@@ -3060,6 +3179,7 @@ async function readUnreadNotifications(agentFilter) {
3060
3179
  project: String(r.project),
3061
3180
  summary: String(r.summary),
3062
3181
  taskFile: r.task_file ? String(r.task_file) : void 0,
3182
+ sessionScope: r.session_scope == null ? null : String(r.session_scope),
3063
3183
  timestamp: String(r.created_at),
3064
3184
  read: false
3065
3185
  }));
@@ -3067,54 +3187,60 @@ async function readUnreadNotifications(agentFilter) {
3067
3187
  return [];
3068
3188
  }
3069
3189
  }
3070
- async function markAsRead(ids) {
3190
+ async function markAsRead(ids, sessionScope) {
3071
3191
  if (ids.length === 0) return;
3072
3192
  try {
3073
3193
  const client = getClient();
3074
3194
  const placeholders = ids.map(() => "?").join(", ");
3195
+ const scope = strictSessionScopeFilter(sessionScope);
3075
3196
  await client.execute({
3076
- sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})`,
3077
- args: ids
3197
+ sql: `UPDATE notifications SET read = 1 WHERE id IN (${placeholders})${scope.sql}`,
3198
+ args: [...ids, ...scope.args]
3078
3199
  });
3079
3200
  } catch {
3080
3201
  }
3081
3202
  }
3082
- async function markAsReadByTaskFile(taskFile) {
3203
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
3083
3204
  try {
3084
3205
  const client = getClient();
3206
+ const scope = strictSessionScopeFilter(sessionScope);
3085
3207
  await client.execute({
3086
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
3087
- args: [taskFile]
3208
+ sql: `UPDATE notifications SET read = 1
3209
+ WHERE task_file = ? AND read = 0${scope.sql}`,
3210
+ args: [taskFile, ...scope.args]
3088
3211
  });
3089
3212
  } catch {
3090
3213
  }
3091
3214
  }
3092
- async function cleanupOldNotifications(daysOld = CLEANUP_DAYS) {
3215
+ async function cleanupOldNotifications(daysOld = CLEANUP_DAYS, sessionScope) {
3093
3216
  try {
3094
3217
  const client = getClient();
3095
3218
  const cutoff = new Date(
3096
3219
  Date.now() - daysOld * 24 * 60 * 60 * 1e3
3097
3220
  ).toISOString();
3221
+ const scope = strictSessionScopeFilter(sessionScope);
3098
3222
  const result = await client.execute({
3099
- sql: "DELETE FROM notifications WHERE created_at < ?",
3100
- args: [cutoff]
3223
+ sql: `DELETE FROM notifications WHERE created_at < ?${scope.sql}`,
3224
+ args: [cutoff, ...scope.args]
3101
3225
  });
3102
3226
  return result.rowsAffected;
3103
3227
  } catch {
3104
3228
  return 0;
3105
3229
  }
3106
3230
  }
3107
- async function markDoneTaskNotificationsAsRead() {
3231
+ async function markDoneTaskNotificationsAsRead(sessionScope) {
3108
3232
  try {
3109
3233
  const client = getClient();
3234
+ const scope = strictSessionScopeFilter(sessionScope);
3110
3235
  const result = await client.execute({
3111
3236
  sql: `UPDATE notifications SET read = 1
3112
3237
  WHERE read = 0
3113
3238
  AND task_file IS NOT NULL
3239
+ ${scope.sql}
3114
3240
  AND task_file IN (
3115
- SELECT task_file FROM tasks WHERE status = 'done'
3241
+ SELECT task_file FROM tasks WHERE status = 'done'${scope.sql}
3116
3242
  )`,
3117
- args: []
3243
+ args: [...scope.args, ...scope.args]
3118
3244
  });
3119
3245
  return result.rowsAffected;
3120
3246
  } catch {
@@ -3145,9 +3271,9 @@ function formatNotifications(notifications) {
3145
3271
  return lines.join("\n");
3146
3272
  }
3147
3273
  async function migrateJsonNotifications() {
3148
- const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path11.join(os7.homedir(), ".exe-os");
3149
- const notifDir = path11.join(base, "notifications");
3150
- if (!existsSync9(notifDir)) return 0;
3274
+ const base = process.env.EXE_OS_DIR || process.env.EXE_MEM_DIR || path12.join(os8.homedir(), ".exe-os");
3275
+ const notifDir = path12.join(base, "notifications");
3276
+ if (!existsSync11(notifDir)) return 0;
3151
3277
  let migrated = 0;
3152
3278
  try {
3153
3279
  const files = readdirSync2(notifDir).filter((f) => f.endsWith(".json"));
@@ -3155,19 +3281,20 @@ async function migrateJsonNotifications() {
3155
3281
  const client = getClient();
3156
3282
  for (const file of files) {
3157
3283
  try {
3158
- const filePath = path11.join(notifDir, file);
3159
- const data = JSON.parse(readFileSync10(filePath, "utf8"));
3284
+ const filePath = path12.join(notifDir, file);
3285
+ const data = JSON.parse(readFileSync11(filePath, "utf8"));
3160
3286
  await client.execute({
3161
- sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
3162
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3287
+ sql: `INSERT OR IGNORE INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
3288
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3163
3289
  args: [
3164
- crypto.randomUUID(),
3290
+ crypto2.randomUUID(),
3165
3291
  data.agentId ?? "unknown",
3166
3292
  data.agentRole ?? "unknown",
3167
3293
  data.event ?? "session_summary",
3168
3294
  data.project ?? "unknown",
3169
3295
  data.summary ?? "",
3170
3296
  data.taskFile ?? null,
3297
+ null,
3171
3298
  data.read ? 1 : 0,
3172
3299
  data.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
3173
3300
  ]
@@ -3221,12 +3348,13 @@ var init_notifications = __esm({
3221
3348
  "src/lib/notifications.ts"() {
3222
3349
  "use strict";
3223
3350
  init_database();
3351
+ init_task_scope();
3224
3352
  CLEANUP_DAYS = 7;
3225
3353
  }
3226
3354
  });
3227
3355
 
3228
3356
  // src/lib/session-kill-telemetry.ts
3229
- import crypto2 from "crypto";
3357
+ import crypto3 from "crypto";
3230
3358
  async function recordSessionKill(input2) {
3231
3359
  try {
3232
3360
  const client = getClient();
@@ -3236,7 +3364,7 @@ async function recordSessionKill(input2) {
3236
3364
  ticks_idle, estimated_tokens_saved)
3237
3365
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
3238
3366
  args: [
3239
- crypto2.randomUUID(),
3367
+ crypto3.randomUUID(),
3240
3368
  input2.sessionName,
3241
3369
  input2.agentId,
3242
3370
  (/* @__PURE__ */ new Date()).toISOString(),
@@ -3331,12 +3459,12 @@ __export(tasks_crud_exports, {
3331
3459
  updateTaskStatus: () => updateTaskStatus,
3332
3460
  writeCheckpoint: () => writeCheckpoint
3333
3461
  });
3334
- import crypto3 from "crypto";
3335
- import path12 from "path";
3336
- import os8 from "os";
3462
+ import crypto4 from "crypto";
3463
+ import path13 from "path";
3464
+ import os9 from "os";
3337
3465
  import { execSync as execSync5 } from "child_process";
3338
3466
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
3339
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
3467
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
3340
3468
  async function writeCheckpoint(input2) {
3341
3469
  const client = getClient();
3342
3470
  const row = await resolveTask(client, input2.taskId);
@@ -3452,7 +3580,7 @@ async function resolveTask(client, identifier, scopeSession) {
3452
3580
  }
3453
3581
  async function createTaskCore(input2) {
3454
3582
  const client = getClient();
3455
- const id = crypto3.randomUUID();
3583
+ const id = crypto4.randomUUID();
3456
3584
  const now = (/* @__PURE__ */ new Date()).toISOString();
3457
3585
  const slug = slugify(input2.title);
3458
3586
  let earlySessionScope = null;
@@ -3511,8 +3639,8 @@ ${laneWarning}` : laneWarning;
3511
3639
  }
3512
3640
  if (input2.baseDir) {
3513
3641
  try {
3514
- await mkdir3(path12.join(input2.baseDir, "exe", "output"), { recursive: true });
3515
- await mkdir3(path12.join(input2.baseDir, "exe", "research"), { recursive: true });
3642
+ await mkdir3(path13.join(input2.baseDir, "exe", "output"), { recursive: true });
3643
+ await mkdir3(path13.join(input2.baseDir, "exe", "research"), { recursive: true });
3516
3644
  await ensureArchitectureDoc(input2.baseDir, input2.projectName);
3517
3645
  await ensureGitignoreExe(input2.baseDir);
3518
3646
  } catch {
@@ -3548,13 +3676,19 @@ ${laneWarning}` : laneWarning;
3548
3676
  });
3549
3677
  if (input2.baseDir) {
3550
3678
  try {
3551
- const EXE_OS_DIR = path12.join(os8.homedir(), ".exe-os");
3552
- const mdPath = path12.join(EXE_OS_DIR, taskFile);
3553
- const mdDir = path12.dirname(mdPath);
3554
- if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
3679
+ const EXE_OS_DIR = path13.join(os9.homedir(), ".exe-os");
3680
+ const mdPath = path13.join(EXE_OS_DIR, taskFile);
3681
+ const mdDir = path13.dirname(mdPath);
3682
+ if (!existsSync12(mdDir)) await mkdir3(mdDir, { recursive: true });
3555
3683
  const reviewer = input2.reviewer ?? input2.assignedBy;
3556
3684
  const mdContent = `# ${input2.title}
3557
3685
 
3686
+ ## MANDATORY: When done
3687
+
3688
+ You MUST call update_task with status "done" and a result summary when finished.
3689
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3690
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
3691
+
3558
3692
  **ID:** ${id}
3559
3693
  **Status:** ${initialStatus}
3560
3694
  **Priority:** ${input2.priority}
@@ -3568,12 +3702,6 @@ ${laneWarning}` : laneWarning;
3568
3702
  ## Context
3569
3703
 
3570
3704
  ${input2.context}
3571
-
3572
- ## MANDATORY: When done
3573
-
3574
- You MUST call update_task with status "done" and a result summary when finished.
3575
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3576
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
3577
3705
  `;
3578
3706
  await writeFile3(mdPath, mdContent, "utf-8");
3579
3707
  } catch (err) {
@@ -3822,7 +3950,7 @@ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
3822
3950
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
3823
3951
  } catch {
3824
3952
  }
3825
- if (input2.status === "done" || input2.status === "cancelled") {
3953
+ if (input2.status === "done" || input2.status === "cancelled" || input2.status === "closed") {
3826
3954
  try {
3827
3955
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
3828
3956
  clearQueueForAgent2(String(row.assigned_to));
@@ -3851,9 +3979,9 @@ async function deleteTaskCore(taskId, _baseDir) {
3851
3979
  return { taskFile, assignedTo, assignedBy, taskSlug };
3852
3980
  }
3853
3981
  async function ensureArchitectureDoc(baseDir, projectName) {
3854
- const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
3982
+ const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
3855
3983
  try {
3856
- if (existsSync10(archPath)) return;
3984
+ if (existsSync12(archPath)) return;
3857
3985
  const template = [
3858
3986
  `# ${projectName} \u2014 System Architecture`,
3859
3987
  "",
@@ -3886,10 +4014,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
3886
4014
  }
3887
4015
  }
3888
4016
  async function ensureGitignoreExe(baseDir) {
3889
- const gitignorePath = path12.join(baseDir, ".gitignore");
4017
+ const gitignorePath = path13.join(baseDir, ".gitignore");
3890
4018
  try {
3891
- if (existsSync10(gitignorePath)) {
3892
- const content = readFileSync11(gitignorePath, "utf-8");
4019
+ if (existsSync12(gitignorePath)) {
4020
+ const content = readFileSync12(gitignorePath, "utf-8");
3893
4021
  if (/^\/?exe\/?$/m.test(content)) return;
3894
4022
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3895
4023
  } else {
@@ -3920,58 +4048,42 @@ var init_tasks_crud = __esm({
3920
4048
  });
3921
4049
 
3922
4050
  // src/lib/tasks-review.ts
3923
- import path13 from "path";
3924
- import { existsSync as existsSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
4051
+ import path14 from "path";
4052
+ import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
3925
4053
  async function countPendingReviews(sessionScope) {
3926
4054
  const client = getClient();
3927
- if (sessionScope) {
3928
- const result2 = await client.execute({
3929
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND session_scope = ?",
3930
- args: [sessionScope]
3931
- });
3932
- return Number(result2.rows[0]?.cnt) || 0;
3933
- }
4055
+ const scope = strictSessionScopeFilter(
4056
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
4057
+ );
3934
4058
  const result = await client.execute({
3935
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3936
- args: []
4059
+ sql: `SELECT COUNT(*) as cnt FROM tasks
4060
+ WHERE status = 'needs_review'${scope.sql}`,
4061
+ args: [...scope.args]
3937
4062
  });
3938
4063
  return Number(result.rows[0]?.cnt) || 0;
3939
4064
  }
3940
4065
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3941
4066
  const client = getClient();
3942
- if (sessionScope) {
3943
- const result2 = await client.execute({
3944
- sql: `SELECT COUNT(*) as cnt FROM tasks
3945
- WHERE status = 'needs_review' AND updated_at > ?
3946
- AND session_scope = ?`,
3947
- args: [sinceIso, sessionScope]
3948
- });
3949
- return Number(result2.rows[0]?.cnt) || 0;
3950
- }
4067
+ const scope = strictSessionScopeFilter(
4068
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
4069
+ );
3951
4070
  const result = await client.execute({
3952
4071
  sql: `SELECT COUNT(*) as cnt FROM tasks
3953
- WHERE status = 'needs_review' AND updated_at > ?`,
3954
- args: [sinceIso]
4072
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
4073
+ args: [sinceIso, ...scope.args]
3955
4074
  });
3956
4075
  return Number(result.rows[0]?.cnt) || 0;
3957
4076
  }
3958
4077
  async function listPendingReviews(limit, sessionScope) {
3959
4078
  const client = getClient();
3960
- if (sessionScope) {
3961
- const result2 = await client.execute({
3962
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3963
- WHERE status = 'needs_review'
3964
- AND session_scope = ?
3965
- ORDER BY updated_at ASC LIMIT ?`,
3966
- args: [sessionScope, limit]
3967
- });
3968
- return result2.rows;
3969
- }
4079
+ const scope = strictSessionScopeFilter(
4080
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
4081
+ );
3970
4082
  const result = await client.execute({
3971
4083
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3972
- WHERE status = 'needs_review'
4084
+ WHERE status = 'needs_review'${scope.sql}
3973
4085
  ORDER BY updated_at ASC LIMIT ?`,
3974
- args: [limit]
4086
+ args: [...scope.args, limit]
3975
4087
  });
3976
4088
  return result.rows;
3977
4089
  }
@@ -3983,7 +4095,7 @@ async function cleanupOrphanedReviews() {
3983
4095
  WHERE status IN ('open', 'needs_review', 'in_progress')
3984
4096
  AND assigned_by = 'system'
3985
4097
  AND title LIKE 'Review:%'
3986
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
4098
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
3987
4099
  args: [now]
3988
4100
  });
3989
4101
  const r1b = await client.execute({
@@ -4102,11 +4214,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4102
4214
  );
4103
4215
  }
4104
4216
  try {
4105
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
4106
- if (existsSync11(cacheDir)) {
4217
+ const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
4218
+ if (existsSync13(cacheDir)) {
4107
4219
  for (const f of readdirSync3(cacheDir)) {
4108
4220
  if (f.startsWith("review-notified-")) {
4109
- unlinkSync5(path13.join(cacheDir, f));
4221
+ unlinkSync5(path14.join(cacheDir, f));
4110
4222
  }
4111
4223
  }
4112
4224
  }
@@ -4123,11 +4235,12 @@ var init_tasks_review = __esm({
4123
4235
  init_tmux_routing();
4124
4236
  init_session_key();
4125
4237
  init_state_bus();
4238
+ init_task_scope();
4126
4239
  }
4127
4240
  });
4128
4241
 
4129
4242
  // src/lib/tasks-chain.ts
4130
- import path14 from "path";
4243
+ import path15 from "path";
4131
4244
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
4132
4245
  async function cascadeUnblock(taskId, baseDir, now) {
4133
4246
  const client = getClient();
@@ -4144,7 +4257,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
4144
4257
  });
4145
4258
  for (const ur of unblockedRows.rows) {
4146
4259
  try {
4147
- const ubFile = path14.join(baseDir, String(ur.task_file));
4260
+ const ubFile = path15.join(baseDir, String(ur.task_file));
4148
4261
  let ubContent = await readFile3(ubFile, "utf-8");
4149
4262
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
4150
4263
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -4179,7 +4292,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
4179
4292
  const scScope = sessionScopeFilter();
4180
4293
  const remaining = await client.execute({
4181
4294
  sql: `SELECT COUNT(*) as cnt FROM tasks
4182
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
4295
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
4183
4296
  args: [parentTaskId, ...scScope.args]
4184
4297
  });
4185
4298
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -4213,7 +4326,7 @@ var init_tasks_chain = __esm({
4213
4326
 
4214
4327
  // src/lib/project-name.ts
4215
4328
  import { execSync as execSync6 } from "child_process";
4216
- import path15 from "path";
4329
+ import path16 from "path";
4217
4330
  function getProjectName(cwd) {
4218
4331
  const dir = cwd ?? process.cwd();
4219
4332
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -4226,7 +4339,7 @@ function getProjectName(cwd) {
4226
4339
  timeout: 2e3,
4227
4340
  stdio: ["pipe", "pipe", "pipe"]
4228
4341
  }).trim();
4229
- repoRoot = path15.dirname(gitCommonDir);
4342
+ repoRoot = path16.dirname(gitCommonDir);
4230
4343
  } catch {
4231
4344
  repoRoot = execSync6("git rev-parse --show-toplevel", {
4232
4345
  cwd: dir,
@@ -4235,11 +4348,11 @@ function getProjectName(cwd) {
4235
4348
  stdio: ["pipe", "pipe", "pipe"]
4236
4349
  }).trim();
4237
4350
  }
4238
- _cached2 = path15.basename(repoRoot);
4351
+ _cached2 = path16.basename(repoRoot);
4239
4352
  _cachedCwd = dir;
4240
4353
  return _cached2;
4241
4354
  } catch {
4242
- _cached2 = path15.basename(dir);
4355
+ _cached2 = path16.basename(dir);
4243
4356
  _cachedCwd = dir;
4244
4357
  return _cached2;
4245
4358
  }
@@ -4382,10 +4495,10 @@ var init_tasks_notify = __esm({
4382
4495
  });
4383
4496
 
4384
4497
  // src/lib/behaviors.ts
4385
- import crypto4 from "crypto";
4498
+ import crypto5 from "crypto";
4386
4499
  async function storeBehavior(opts) {
4387
4500
  const client = getClient();
4388
- const id = crypto4.randomUUID();
4501
+ const id = crypto5.randomUUID();
4389
4502
  const now = (/* @__PURE__ */ new Date()).toISOString();
4390
4503
  await client.execute({
4391
4504
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -4414,7 +4527,7 @@ __export(skill_learning_exports, {
4414
4527
  storeTrajectory: () => storeTrajectory,
4415
4528
  sweepTrajectories: () => sweepTrajectories
4416
4529
  });
4417
- import crypto5 from "crypto";
4530
+ import crypto6 from "crypto";
4418
4531
  async function extractTrajectory(taskId, agentId) {
4419
4532
  const client = getClient();
4420
4533
  const result = await client.execute({
@@ -4443,11 +4556,11 @@ async function extractTrajectory(taskId, agentId) {
4443
4556
  return signature;
4444
4557
  }
4445
4558
  function hashSignature(signature) {
4446
- return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4559
+ return crypto6.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4447
4560
  }
4448
4561
  async function storeTrajectory(opts) {
4449
4562
  const client = getClient();
4450
- const id = crypto5.randomUUID();
4563
+ const id = crypto6.randomUUID();
4451
4564
  const now = (/* @__PURE__ */ new Date()).toISOString();
4452
4565
  const signatureHash = hashSignature(opts.signature);
4453
4566
  await client.execute({
@@ -4712,8 +4825,8 @@ __export(tasks_exports, {
4712
4825
  updateTaskStatus: () => updateTaskStatus,
4713
4826
  writeCheckpoint: () => writeCheckpoint
4714
4827
  });
4715
- import path16 from "path";
4716
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync6 } from "fs";
4828
+ import path17 from "path";
4829
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync6 } from "fs";
4717
4830
  async function createTask(input2) {
4718
4831
  const result = await createTaskCore(input2);
4719
4832
  if (!input2.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -4732,12 +4845,12 @@ async function updateTask(input2) {
4732
4845
  const { row, taskFile, now, taskId } = await updateTaskStatus(input2);
4733
4846
  try {
4734
4847
  const agent = String(row.assigned_to);
4735
- const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
4736
- const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
4848
+ const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
4849
+ const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
4737
4850
  if (input2.status === "in_progress") {
4738
4851
  mkdirSync6(cacheDir, { recursive: true });
4739
- writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4740
- } else if (input2.status === "done" || input2.status === "blocked" || input2.status === "cancelled") {
4852
+ writeFileSync8(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4853
+ } else if (input2.status === "done" || input2.status === "blocked" || input2.status === "cancelled" || input2.status === "closed") {
4741
4854
  try {
4742
4855
  unlinkSync6(cachePath);
4743
4856
  } catch {
@@ -4745,10 +4858,10 @@ async function updateTask(input2) {
4745
4858
  }
4746
4859
  } catch {
4747
4860
  }
4748
- if (input2.status === "done") {
4861
+ if (input2.status === "done" || input2.status === "closed") {
4749
4862
  await cleanupReviewFile(row, taskFile, input2.baseDir);
4750
4863
  }
4751
- if (input2.status === "done" || input2.status === "cancelled") {
4864
+ if (input2.status === "done" || input2.status === "cancelled" || input2.status === "closed") {
4752
4865
  try {
4753
4866
  const client = getClient();
4754
4867
  const taskTitle = String(row.title);
@@ -4764,7 +4877,7 @@ async function updateTask(input2) {
4764
4877
  if (!isCoordinatorName(assignedAgent)) {
4765
4878
  try {
4766
4879
  const draftClient = getClient();
4767
- if (input2.status === "done") {
4880
+ if (input2.status === "done" || input2.status === "closed") {
4768
4881
  await draftClient.execute({
4769
4882
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
4770
4883
  args: [assignedAgent]
@@ -4781,7 +4894,7 @@ async function updateTask(input2) {
4781
4894
  try {
4782
4895
  const client = getClient();
4783
4896
  const cascaded = await client.execute({
4784
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
4897
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
4785
4898
  WHERE parent_task_id = ? AND status = 'needs_review'`,
4786
4899
  args: [now, taskId]
4787
4900
  });
@@ -4794,14 +4907,14 @@ async function updateTask(input2) {
4794
4907
  } catch {
4795
4908
  }
4796
4909
  }
4797
- const isTerminal = input2.status === "done" || input2.status === "needs_review";
4910
+ const isTerminal = input2.status === "done" || input2.status === "needs_review" || input2.status === "closed";
4798
4911
  if (isTerminal) {
4799
4912
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
4800
4913
  if (!isCoordinator) {
4801
4914
  notifyTaskDone();
4802
4915
  }
4803
4916
  await markTaskNotificationsRead(taskFile);
4804
- if (input2.status === "done") {
4917
+ if (input2.status === "done" || input2.status === "closed") {
4805
4918
  try {
4806
4919
  await cascadeUnblock(taskId, input2.baseDir, now);
4807
4920
  } catch {
@@ -4821,7 +4934,7 @@ async function updateTask(input2) {
4821
4934
  }
4822
4935
  }
4823
4936
  }
4824
- if (input2.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4937
+ if ((input2.status === "done" || input2.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4825
4938
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4826
4939
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4827
4940
  taskId,
@@ -5193,6 +5306,7 @@ __export(tmux_routing_exports, {
5193
5306
  isEmployeeAlive: () => isEmployeeAlive,
5194
5307
  isExeSession: () => isExeSession,
5195
5308
  isSessionBusy: () => isSessionBusy,
5309
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
5196
5310
  notifyParentExe: () => notifyParentExe,
5197
5311
  parseParentExe: () => parseParentExe,
5198
5312
  registerParentExe: () => registerParentExe,
@@ -5203,13 +5317,13 @@ __export(tmux_routing_exports, {
5203
5317
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
5204
5318
  });
5205
5319
  import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
5206
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, existsSync as existsSync12, appendFileSync, readdirSync as readdirSync4 } from "fs";
5207
- import path17 from "path";
5208
- import os9 from "os";
5320
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, existsSync as existsSync14, appendFileSync, readdirSync as readdirSync4 } from "fs";
5321
+ import path18 from "path";
5322
+ import os10 from "os";
5209
5323
  import { fileURLToPath as fileURLToPath2 } from "url";
5210
5324
  import { unlinkSync as unlinkSync7 } from "fs";
5211
5325
  function spawnLockPath(sessionName) {
5212
- return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5326
+ return path18.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5213
5327
  }
5214
5328
  function isProcessAlive(pid) {
5215
5329
  try {
@@ -5220,13 +5334,13 @@ function isProcessAlive(pid) {
5220
5334
  }
5221
5335
  }
5222
5336
  function acquireSpawnLock2(sessionName) {
5223
- if (!existsSync12(SPAWN_LOCK_DIR)) {
5337
+ if (!existsSync14(SPAWN_LOCK_DIR)) {
5224
5338
  mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
5225
5339
  }
5226
5340
  const lockFile = spawnLockPath(sessionName);
5227
- if (existsSync12(lockFile)) {
5341
+ if (existsSync14(lockFile)) {
5228
5342
  try {
5229
- const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
5343
+ const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
5230
5344
  const age = Date.now() - lock.timestamp;
5231
5345
  if (isProcessAlive(lock.pid) && age < 6e4) {
5232
5346
  return false;
@@ -5234,7 +5348,7 @@ function acquireSpawnLock2(sessionName) {
5234
5348
  } catch {
5235
5349
  }
5236
5350
  }
5237
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5351
+ writeFileSync9(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5238
5352
  return true;
5239
5353
  }
5240
5354
  function releaseSpawnLock2(sessionName) {
@@ -5246,13 +5360,13 @@ function releaseSpawnLock2(sessionName) {
5246
5360
  function resolveBehaviorsExporterScript() {
5247
5361
  try {
5248
5362
  const thisFile = fileURLToPath2(import.meta.url);
5249
- const scriptPath = path17.join(
5250
- path17.dirname(thisFile),
5363
+ const scriptPath = path18.join(
5364
+ path18.dirname(thisFile),
5251
5365
  "..",
5252
5366
  "bin",
5253
5367
  "exe-export-behaviors.js"
5254
5368
  );
5255
- return existsSync12(scriptPath) ? scriptPath : null;
5369
+ return existsSync14(scriptPath) ? scriptPath : null;
5256
5370
  } catch {
5257
5371
  return null;
5258
5372
  }
@@ -5318,12 +5432,12 @@ function extractRootExe(name) {
5318
5432
  return parts.length > 0 ? parts[parts.length - 1] : null;
5319
5433
  }
5320
5434
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5321
- if (!existsSync12(SESSION_CACHE)) {
5435
+ if (!existsSync14(SESSION_CACHE)) {
5322
5436
  mkdirSync7(SESSION_CACHE, { recursive: true });
5323
5437
  }
5324
5438
  const rootExe = extractRootExe(parentExe) ?? parentExe;
5325
- const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5326
- writeFileSync8(filePath, JSON.stringify({
5439
+ const filePath = path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5440
+ writeFileSync9(filePath, JSON.stringify({
5327
5441
  parentExe: rootExe,
5328
5442
  dispatchedBy: dispatchedBy || rootExe,
5329
5443
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5331,7 +5445,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5331
5445
  }
5332
5446
  function getParentExe(sessionKey) {
5333
5447
  try {
5334
- const data = JSON.parse(readFileSync12(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5448
+ const data = JSON.parse(readFileSync13(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5335
5449
  return data.parentExe || null;
5336
5450
  } catch {
5337
5451
  return null;
@@ -5339,8 +5453,8 @@ function getParentExe(sessionKey) {
5339
5453
  }
5340
5454
  function getDispatchedBy(sessionKey) {
5341
5455
  try {
5342
- const data = JSON.parse(readFileSync12(
5343
- path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5456
+ const data = JSON.parse(readFileSync13(
5457
+ path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5344
5458
  "utf8"
5345
5459
  ));
5346
5460
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -5410,8 +5524,8 @@ async function verifyPaneAtCapacity(sessionName) {
5410
5524
  }
5411
5525
  function readDebounceState() {
5412
5526
  try {
5413
- if (!existsSync12(DEBOUNCE_FILE)) return {};
5414
- const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
5527
+ if (!existsSync14(DEBOUNCE_FILE)) return {};
5528
+ const raw = JSON.parse(readFileSync13(DEBOUNCE_FILE, "utf8"));
5415
5529
  const state = {};
5416
5530
  for (const [key, val] of Object.entries(raw)) {
5417
5531
  if (typeof val === "number") {
@@ -5427,8 +5541,8 @@ function readDebounceState() {
5427
5541
  }
5428
5542
  function writeDebounceState(state) {
5429
5543
  try {
5430
- if (!existsSync12(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
5431
- writeFileSync8(DEBOUNCE_FILE, JSON.stringify(state));
5544
+ if (!existsSync14(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
5545
+ writeFileSync9(DEBOUNCE_FILE, JSON.stringify(state));
5432
5546
  } catch {
5433
5547
  }
5434
5548
  }
@@ -5526,8 +5640,8 @@ function sendIntercom(targetSession) {
5526
5640
  try {
5527
5641
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5528
5642
  const agent = baseAgentName(rawAgent);
5529
- const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5530
- if (existsSync12(markerPath)) {
5643
+ const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
5644
+ if (existsSync14(markerPath)) {
5531
5645
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5532
5646
  return "debounced";
5533
5647
  }
@@ -5536,8 +5650,8 @@ function sendIntercom(targetSession) {
5536
5650
  try {
5537
5651
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5538
5652
  const agent = baseAgentName(rawAgent);
5539
- const taskDir = path17.join(process.cwd(), "exe", agent);
5540
- if (existsSync12(taskDir)) {
5653
+ const taskDir = path18.join(process.cwd(), "exe", agent);
5654
+ if (existsSync14(taskDir)) {
5541
5655
  const files = readdirSync4(taskDir).filter(
5542
5656
  (f) => f.endsWith(".md") && f !== "DONE.txt"
5543
5657
  );
@@ -5597,6 +5711,21 @@ function notifyParentExe(sessionKey) {
5597
5711
  }
5598
5712
  return true;
5599
5713
  }
5714
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
5715
+ const transport = getTransport();
5716
+ try {
5717
+ const sessions = transport.listSessions();
5718
+ if (!sessions.includes(coordinatorSession)) return false;
5719
+ execSync7(
5720
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5721
+ { timeout: 3e3 }
5722
+ );
5723
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
5724
+ return true;
5725
+ } catch {
5726
+ return false;
5727
+ }
5728
+ }
5600
5729
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
5601
5730
  if (isCoordinatorName(employeeName)) {
5602
5731
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -5670,26 +5799,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5670
5799
  const transport = getTransport();
5671
5800
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
5672
5801
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
5673
- const logDir = path17.join(os9.homedir(), ".exe-os", "session-logs");
5674
- const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5675
- if (!existsSync12(logDir)) {
5802
+ const logDir = path18.join(os10.homedir(), ".exe-os", "session-logs");
5803
+ const logFile = path18.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5804
+ if (!existsSync14(logDir)) {
5676
5805
  mkdirSync7(logDir, { recursive: true });
5677
5806
  }
5678
5807
  transport.kill(sessionName);
5679
5808
  let cleanupSuffix = "";
5680
5809
  try {
5681
5810
  const thisFile = fileURLToPath2(import.meta.url);
5682
- const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5683
- if (existsSync12(cleanupScript)) {
5811
+ const cleanupScript = path18.join(path18.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5812
+ if (existsSync14(cleanupScript)) {
5684
5813
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
5685
5814
  }
5686
5815
  } catch {
5687
5816
  }
5688
5817
  try {
5689
- const claudeJsonPath = path17.join(os9.homedir(), ".claude.json");
5818
+ const claudeJsonPath = path18.join(os10.homedir(), ".claude.json");
5690
5819
  let claudeJson = {};
5691
5820
  try {
5692
- claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
5821
+ claudeJson = JSON.parse(readFileSync13(claudeJsonPath, "utf8"));
5693
5822
  } catch {
5694
5823
  }
5695
5824
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -5697,17 +5826,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5697
5826
  const trustDir = opts?.cwd ?? projectDir;
5698
5827
  if (!projects[trustDir]) projects[trustDir] = {};
5699
5828
  projects[trustDir].hasTrustDialogAccepted = true;
5700
- writeFileSync8(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5829
+ writeFileSync9(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5701
5830
  } catch {
5702
5831
  }
5703
5832
  try {
5704
- const settingsDir = path17.join(os9.homedir(), ".claude", "projects");
5833
+ const settingsDir = path18.join(os10.homedir(), ".claude", "projects");
5705
5834
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
5706
- const projSettingsDir = path17.join(settingsDir, normalizedKey);
5707
- const settingsPath = path17.join(projSettingsDir, "settings.json");
5835
+ const projSettingsDir = path18.join(settingsDir, normalizedKey);
5836
+ const settingsPath = path18.join(projSettingsDir, "settings.json");
5708
5837
  let settings = {};
5709
5838
  try {
5710
- settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
5839
+ settings = JSON.parse(readFileSync13(settingsPath, "utf8"));
5711
5840
  } catch {
5712
5841
  }
5713
5842
  const perms = settings.permissions ?? {};
@@ -5736,7 +5865,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5736
5865
  perms.allow = allow;
5737
5866
  settings.permissions = perms;
5738
5867
  mkdirSync7(projSettingsDir, { recursive: true });
5739
- writeFileSync8(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5868
+ writeFileSync9(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5740
5869
  }
5741
5870
  } catch {
5742
5871
  }
@@ -5751,8 +5880,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5751
5880
  let behaviorsFlag = "";
5752
5881
  let legacyFallbackWarned = false;
5753
5882
  if (!useExeAgent && !useBinSymlink) {
5754
- const identityPath = path17.join(
5755
- os9.homedir(),
5883
+ const identityPath = path18.join(
5884
+ os10.homedir(),
5756
5885
  ".exe-os",
5757
5886
  "identity",
5758
5887
  `${employeeName}.md`
@@ -5761,13 +5890,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5761
5890
  const hasAgentFlag = claudeSupportsAgentFlag();
5762
5891
  if (hasAgentFlag) {
5763
5892
  identityFlag = ` --agent ${employeeName}`;
5764
- } else if (existsSync12(identityPath)) {
5893
+ } else if (existsSync14(identityPath)) {
5765
5894
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
5766
5895
  legacyFallbackWarned = true;
5767
5896
  }
5768
5897
  const behaviorsFile = exportBehaviorsSync(
5769
5898
  employeeName,
5770
- path17.basename(spawnCwd),
5899
+ path18.basename(spawnCwd),
5771
5900
  sessionName
5772
5901
  );
5773
5902
  if (behaviorsFile) {
@@ -5782,16 +5911,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5782
5911
  }
5783
5912
  let sessionContextFlag = "";
5784
5913
  try {
5785
- const ctxDir = path17.join(os9.homedir(), ".exe-os", "session-cache");
5914
+ const ctxDir = path18.join(os10.homedir(), ".exe-os", "session-cache");
5786
5915
  mkdirSync7(ctxDir, { recursive: true });
5787
- const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
5916
+ const ctxFile = path18.join(ctxDir, `session-context-${sessionName}.md`);
5788
5917
  const ctxContent = [
5789
5918
  `## Session Context`,
5790
5919
  `You are running in tmux session: ${sessionName}.`,
5791
5920
  `Your parent coordinator session is ${exeSession}.`,
5792
5921
  `Your employees (if any) use the -${exeSession} suffix.`
5793
5922
  ].join("\n");
5794
- writeFileSync8(ctxFile, ctxContent);
5923
+ writeFileSync9(ctxFile, ctxContent);
5795
5924
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5796
5925
  } catch {
5797
5926
  }
@@ -5868,8 +5997,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5868
5997
  transport.pipeLog(sessionName, logFile);
5869
5998
  try {
5870
5999
  const mySession = getMySession();
5871
- const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5872
- writeFileSync8(dispatchInfo, JSON.stringify({
6000
+ const dispatchInfo = path18.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
6001
+ writeFileSync9(dispatchInfo, JSON.stringify({
5873
6002
  dispatchedBy: mySession,
5874
6003
  rootExe: exeSession,
5875
6004
  provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
@@ -5943,15 +6072,15 @@ var init_tmux_routing = __esm({
5943
6072
  init_intercom_queue();
5944
6073
  init_plan_limits();
5945
6074
  init_employees();
5946
- SPAWN_LOCK_DIR = path17.join(os9.homedir(), ".exe-os", "spawn-locks");
5947
- SESSION_CACHE = path17.join(os9.homedir(), ".exe-os", "session-cache");
6075
+ SPAWN_LOCK_DIR = path18.join(os10.homedir(), ".exe-os", "spawn-locks");
6076
+ SESSION_CACHE = path18.join(os10.homedir(), ".exe-os", "session-cache");
5948
6077
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5949
6078
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
5950
6079
  VERIFY_PANE_LINES = 200;
5951
6080
  INTERCOM_DEBOUNCE_MS = 3e4;
5952
6081
  CODEX_DEBOUNCE_MS = 12e4;
5953
- INTERCOM_LOG2 = path17.join(os9.homedir(), ".exe-os", "intercom.log");
5954
- DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
6082
+ INTERCOM_LOG2 = path18.join(os10.homedir(), ".exe-os", "intercom.log");
6083
+ DEBOUNCE_FILE = path18.join(SESSION_CACHE, "intercom-debounce.json");
5955
6084
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5956
6085
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
5957
6086
  }
@@ -5974,6 +6103,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
5974
6103
  args: [scope]
5975
6104
  };
5976
6105
  }
6106
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
6107
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
6108
+ if (!scope) return { sql: "", args: [] };
6109
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
6110
+ return {
6111
+ sql: ` AND ${col} = ?`,
6112
+ args: [scope]
6113
+ };
6114
+ }
5977
6115
  var init_task_scope = __esm({
5978
6116
  "src/lib/task-scope.ts"() {
5979
6117
  "use strict";
@@ -5992,14 +6130,14 @@ var init_memory = __esm({
5992
6130
 
5993
6131
  // src/lib/keychain.ts
5994
6132
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
5995
- import { existsSync as existsSync13 } from "fs";
5996
- import path18 from "path";
5997
- import os10 from "os";
6133
+ import { existsSync as existsSync15 } from "fs";
6134
+ import path19 from "path";
6135
+ import os11 from "os";
5998
6136
  function getKeyDir() {
5999
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path18.join(os10.homedir(), ".exe-os");
6137
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path19.join(os11.homedir(), ".exe-os");
6000
6138
  }
6001
6139
  function getKeyPath() {
6002
- return path18.join(getKeyDir(), "master.key");
6140
+ return path19.join(getKeyDir(), "master.key");
6003
6141
  }
6004
6142
  async function tryKeytar() {
6005
6143
  try {
@@ -6020,9 +6158,9 @@ async function getMasterKey() {
6020
6158
  }
6021
6159
  }
6022
6160
  const keyPath = getKeyPath();
6023
- if (!existsSync13(keyPath)) {
6161
+ if (!existsSync15(keyPath)) {
6024
6162
  process.stderr.write(
6025
- `[keychain] Key not found at ${keyPath} (HOME=${os10.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
6163
+ `[keychain] Key not found at ${keyPath} (HOME=${os11.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
6026
6164
  `
6027
6165
  );
6028
6166
  return null;
@@ -6052,6 +6190,7 @@ var shard_manager_exports = {};
6052
6190
  __export(shard_manager_exports, {
6053
6191
  disposeShards: () => disposeShards,
6054
6192
  ensureShardSchema: () => ensureShardSchema,
6193
+ getOpenShardCount: () => getOpenShardCount,
6055
6194
  getReadyShardClient: () => getReadyShardClient,
6056
6195
  getShardClient: () => getShardClient,
6057
6196
  getShardsDir: () => getShardsDir,
@@ -6060,15 +6199,18 @@ __export(shard_manager_exports, {
6060
6199
  listShards: () => listShards,
6061
6200
  shardExists: () => shardExists
6062
6201
  });
6063
- import path19 from "path";
6064
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync5 } from "fs";
6202
+ import path20 from "path";
6203
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync5 } from "fs";
6065
6204
  import { createClient as createClient2 } from "@libsql/client";
6066
6205
  function initShardManager(encryptionKey) {
6067
6206
  _encryptionKey = encryptionKey;
6068
- if (!existsSync14(SHARDS_DIR)) {
6207
+ if (!existsSync16(SHARDS_DIR)) {
6069
6208
  mkdirSync8(SHARDS_DIR, { recursive: true });
6070
6209
  }
6071
6210
  _shardingEnabled = true;
6211
+ if (_evictionTimer) clearInterval(_evictionTimer);
6212
+ _evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
6213
+ _evictionTimer.unref();
6072
6214
  }
6073
6215
  function isShardingEnabled() {
6074
6216
  return _shardingEnabled;
@@ -6085,21 +6227,28 @@ function getShardClient(projectName) {
6085
6227
  throw new Error(`Invalid project name for shard: "${projectName}"`);
6086
6228
  }
6087
6229
  const cached = _shards.get(safeName);
6088
- if (cached) return cached;
6089
- const dbPath = path19.join(SHARDS_DIR, `${safeName}.db`);
6230
+ if (cached) {
6231
+ _shardLastAccess.set(safeName, Date.now());
6232
+ return cached;
6233
+ }
6234
+ while (_shards.size >= MAX_OPEN_SHARDS) {
6235
+ evictLRU();
6236
+ }
6237
+ const dbPath = path20.join(SHARDS_DIR, `${safeName}.db`);
6090
6238
  const client = createClient2({
6091
6239
  url: `file:${dbPath}`,
6092
6240
  encryptionKey: _encryptionKey
6093
6241
  });
6094
6242
  _shards.set(safeName, client);
6243
+ _shardLastAccess.set(safeName, Date.now());
6095
6244
  return client;
6096
6245
  }
6097
6246
  function shardExists(projectName) {
6098
6247
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
6099
- return existsSync14(path19.join(SHARDS_DIR, `${safeName}.db`));
6248
+ return existsSync16(path20.join(SHARDS_DIR, `${safeName}.db`));
6100
6249
  }
6101
6250
  function listShards() {
6102
- if (!existsSync14(SHARDS_DIR)) return [];
6251
+ if (!existsSync16(SHARDS_DIR)) return [];
6103
6252
  return readdirSync5(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
6104
6253
  }
6105
6254
  async function ensureShardSchema(client) {
@@ -6151,6 +6300,8 @@ async function ensureShardSchema(client) {
6151
6300
  for (const col of [
6152
6301
  "ALTER TABLE memories ADD COLUMN task_id TEXT",
6153
6302
  "ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
6303
+ "ALTER TABLE memories ADD COLUMN author_device_id TEXT",
6304
+ "ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
6154
6305
  "ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
6155
6306
  "ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
6156
6307
  "ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
@@ -6288,21 +6439,69 @@ async function getReadyShardClient(projectName) {
6288
6439
  await ensureShardSchema(client);
6289
6440
  return client;
6290
6441
  }
6442
+ function evictLRU() {
6443
+ let oldest = null;
6444
+ let oldestTime = Infinity;
6445
+ for (const [name, time] of _shardLastAccess) {
6446
+ if (time < oldestTime) {
6447
+ oldestTime = time;
6448
+ oldest = name;
6449
+ }
6450
+ }
6451
+ if (oldest) {
6452
+ const client = _shards.get(oldest);
6453
+ if (client) {
6454
+ client.close();
6455
+ }
6456
+ _shards.delete(oldest);
6457
+ _shardLastAccess.delete(oldest);
6458
+ }
6459
+ }
6460
+ function evictIdleShards() {
6461
+ const now = Date.now();
6462
+ const toEvict = [];
6463
+ for (const [name, lastAccess] of _shardLastAccess) {
6464
+ if (now - lastAccess > SHARD_IDLE_MS) {
6465
+ toEvict.push(name);
6466
+ }
6467
+ }
6468
+ for (const name of toEvict) {
6469
+ const client = _shards.get(name);
6470
+ if (client) {
6471
+ client.close();
6472
+ }
6473
+ _shards.delete(name);
6474
+ _shardLastAccess.delete(name);
6475
+ }
6476
+ }
6477
+ function getOpenShardCount() {
6478
+ return _shards.size;
6479
+ }
6291
6480
  function disposeShards() {
6481
+ if (_evictionTimer) {
6482
+ clearInterval(_evictionTimer);
6483
+ _evictionTimer = null;
6484
+ }
6292
6485
  for (const [, client] of _shards) {
6293
6486
  client.close();
6294
6487
  }
6295
6488
  _shards.clear();
6489
+ _shardLastAccess.clear();
6296
6490
  _shardingEnabled = false;
6297
6491
  _encryptionKey = null;
6298
6492
  }
6299
- var SHARDS_DIR, _shards, _encryptionKey, _shardingEnabled;
6493
+ var SHARDS_DIR, SHARD_IDLE_MS, MAX_OPEN_SHARDS, EVICTION_INTERVAL_MS, _shards, _shardLastAccess, _evictionTimer, _encryptionKey, _shardingEnabled;
6300
6494
  var init_shard_manager = __esm({
6301
6495
  "src/lib/shard-manager.ts"() {
6302
6496
  "use strict";
6303
6497
  init_config();
6304
- SHARDS_DIR = path19.join(EXE_AI_DIR, "shards");
6498
+ SHARDS_DIR = path20.join(EXE_AI_DIR, "shards");
6499
+ SHARD_IDLE_MS = 5 * 60 * 1e3;
6500
+ MAX_OPEN_SHARDS = 10;
6501
+ EVICTION_INTERVAL_MS = 60 * 1e3;
6305
6502
  _shards = /* @__PURE__ */ new Map();
6503
+ _shardLastAccess = /* @__PURE__ */ new Map();
6504
+ _evictionTimer = null;
6306
6505
  _encryptionKey = null;
6307
6506
  _shardingEnabled = false;
6308
6507
  }
@@ -7287,7 +7486,7 @@ var init_git_task_sweep = __esm({
7287
7486
  init_config();
7288
7487
  init_session_key();
7289
7488
  init_employees();
7290
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync, unlinkSync as unlinkSync2, readdirSync } from "fs";
7489
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, readdirSync } from "fs";
7291
7490
  import { execSync as execSync3 } from "child_process";
7292
7491
  import path3 from "path";
7293
7492
  var CACHE_DIR = path3.join(EXE_AI_DIR, "session-cache");
@@ -7418,7 +7617,7 @@ process.stdin.on("end", async () => {
7418
7617
  const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
7419
7618
  const { randomUUID: randomUUID4 } = await import("crypto");
7420
7619
  const client = getClient2();
7421
- const seScope = sessionScopeFilter();
7620
+ const seScope = strictSessionScopeFilter();
7422
7621
  const orphanResult = await client.execute({
7423
7622
  sql: `SELECT title, status FROM tasks WHERE assigned_to = ? AND status IN ('open', 'in_progress')${seScope.sql}`,
7424
7623
  args: [agent.agentId, ...seScope.args]
@@ -7462,8 +7661,10 @@ Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title
7462
7661
  let context;
7463
7662
  try {
7464
7663
  const ctxResult = await client.execute({
7465
- sql: "SELECT id, context FROM tasks WHERE title = ? AND assigned_to = ? AND status = 'in_progress' LIMIT 1",
7466
- args: [title, agent.agentId]
7664
+ sql: `SELECT id, context FROM tasks
7665
+ WHERE title = ? AND assigned_to = ? AND status = 'in_progress'${seScope.sql}
7666
+ LIMIT 1`,
7667
+ args: [title, agent.agentId, ...seScope.args]
7467
7668
  });
7468
7669
  if (ctxResult.rows.length > 0) {
7469
7670
  context = ctxResult.rows[0].context ? String(ctxResult.rows[0].context) : void 0;
@@ -7474,12 +7675,14 @@ Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title
7474
7675
  const match = findBestMatch2(taskForMatch, commits);
7475
7676
  if (match) {
7476
7677
  await client.execute({
7477
- sql: "UPDATE tasks SET status = 'done', result = ?, updated_at = ? WHERE title = ? AND assigned_to = ? AND status = 'in_progress'",
7678
+ sql: `UPDATE tasks SET status = 'done', result = ?, updated_at = ?
7679
+ WHERE title = ? AND assigned_to = ? AND status = 'in_progress'${seScope.sql}`,
7478
7680
  args: [
7479
7681
  `Auto-closed: session ended but matching commit ${match.commit.hash} found (score: ${match.score.toFixed(2)}). Message: "${match.commit.message}"`,
7480
7682
  (/* @__PURE__ */ new Date()).toISOString(),
7481
7683
  title,
7482
- agent.agentId
7684
+ agent.agentId,
7685
+ ...seScope.args
7483
7686
  ]
7484
7687
  });
7485
7688
  autoClosed.push(`"${title}" \u2192 commit ${match.commit.hash}`);