@askexenow/exe-os 0.9.8 → 0.9.10

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 +1411 -953
  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 +913 -543
  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 +418 -262
  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 +793 -485
  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 +566 -357
  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 +530 -319
  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 +547 -336
  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 +649 -417
  44. package/dist/hooks/bug-report-worker.js +486 -316
  45. package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
  46. package/dist/hooks/commit-complete.js +528 -317
  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 +3442 -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 +534 -323
  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 +614 -382
  58. package/dist/hooks/response-ingest-worker.js +372 -122
  59. package/dist/hooks/session-end.js +569 -347
  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 +664 -431
  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 +1049 -680
  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 +422 -357
  88. package/dist/lib/tmux-routing.js +314 -248
  89. package/dist/lib/token-spend.js +26 -8
  90. package/dist/mcp/server.js +1408 -672
  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 +448 -371
  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 +1983 -315
  99. package/dist/runtime/index.js +567 -355
  100. package/dist/tui/App.js +887 -531
  101. package/package.json +4 -4
@@ -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(),
@@ -3314,6 +3442,110 @@ var init_state_bus = __esm({
3314
3442
  }
3315
3443
  });
3316
3444
 
3445
+ // src/lib/project-name.ts
3446
+ import { execSync as execSync5 } from "child_process";
3447
+ import path13 from "path";
3448
+ function getProjectName(cwd) {
3449
+ const dir = cwd ?? process.cwd();
3450
+ if (_cached2 && _cachedCwd === dir) return _cached2;
3451
+ try {
3452
+ let repoRoot;
3453
+ try {
3454
+ const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
3455
+ cwd: dir,
3456
+ encoding: "utf8",
3457
+ timeout: 2e3,
3458
+ stdio: ["pipe", "pipe", "pipe"]
3459
+ }).trim();
3460
+ repoRoot = path13.dirname(gitCommonDir);
3461
+ } catch {
3462
+ repoRoot = execSync5("git rev-parse --show-toplevel", {
3463
+ cwd: dir,
3464
+ encoding: "utf8",
3465
+ timeout: 2e3,
3466
+ stdio: ["pipe", "pipe", "pipe"]
3467
+ }).trim();
3468
+ }
3469
+ _cached2 = path13.basename(repoRoot);
3470
+ _cachedCwd = dir;
3471
+ return _cached2;
3472
+ } catch {
3473
+ _cached2 = path13.basename(dir);
3474
+ _cachedCwd = dir;
3475
+ return _cached2;
3476
+ }
3477
+ }
3478
+ var _cached2, _cachedCwd;
3479
+ var init_project_name = __esm({
3480
+ "src/lib/project-name.ts"() {
3481
+ "use strict";
3482
+ _cached2 = null;
3483
+ _cachedCwd = null;
3484
+ }
3485
+ });
3486
+
3487
+ // src/lib/session-scope.ts
3488
+ var session_scope_exports = {};
3489
+ __export(session_scope_exports, {
3490
+ assertSessionScope: () => assertSessionScope,
3491
+ findSessionForProject: () => findSessionForProject,
3492
+ getSessionProject: () => getSessionProject
3493
+ });
3494
+ function getSessionProject(sessionName) {
3495
+ const sessions = listSessions();
3496
+ const entry = sessions.find((s) => s.windowName === sessionName);
3497
+ if (!entry) return null;
3498
+ const parts = entry.projectDir.split("/").filter(Boolean);
3499
+ return parts[parts.length - 1] ?? null;
3500
+ }
3501
+ function findSessionForProject(projectName) {
3502
+ const sessions = listSessions();
3503
+ for (const s of sessions) {
3504
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
3505
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
3506
+ }
3507
+ return null;
3508
+ }
3509
+ function assertSessionScope(actionType, targetProject) {
3510
+ try {
3511
+ const currentProject = getProjectName();
3512
+ const exeSession = resolveExeSession();
3513
+ if (!exeSession) {
3514
+ return { allowed: true, reason: "no_session" };
3515
+ }
3516
+ if (currentProject === targetProject) {
3517
+ return {
3518
+ allowed: true,
3519
+ reason: "same_session",
3520
+ currentProject,
3521
+ targetProject
3522
+ };
3523
+ }
3524
+ process.stderr.write(
3525
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
3526
+ `
3527
+ );
3528
+ return {
3529
+ allowed: false,
3530
+ reason: "cross_session_denied",
3531
+ currentProject,
3532
+ targetProject,
3533
+ targetSession: findSessionForProject(targetProject)?.windowName
3534
+ };
3535
+ } catch {
3536
+ return { allowed: true, reason: "no_session" };
3537
+ }
3538
+ }
3539
+ var init_session_scope = __esm({
3540
+ "src/lib/session-scope.ts"() {
3541
+ "use strict";
3542
+ init_session_registry();
3543
+ init_project_name();
3544
+ init_tmux_routing();
3545
+ init_employees();
3546
+ }
3547
+ });
3548
+
3317
3549
  // src/lib/tasks-crud.ts
3318
3550
  var tasks_crud_exports = {};
3319
3551
  __export(tasks_crud_exports, {
@@ -3331,12 +3563,12 @@ __export(tasks_crud_exports, {
3331
3563
  updateTaskStatus: () => updateTaskStatus,
3332
3564
  writeCheckpoint: () => writeCheckpoint
3333
3565
  });
3334
- import crypto3 from "crypto";
3335
- import path12 from "path";
3336
- import os8 from "os";
3337
- import { execSync as execSync5 } from "child_process";
3566
+ import crypto4 from "crypto";
3567
+ import path14 from "path";
3568
+ import os9 from "os";
3569
+ import { execSync as execSync6 } from "child_process";
3338
3570
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
3339
- import { existsSync as existsSync10, readFileSync as readFileSync11 } from "fs";
3571
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
3340
3572
  async function writeCheckpoint(input2) {
3341
3573
  const client = getClient();
3342
3574
  const row = await resolveTask(client, input2.taskId);
@@ -3452,13 +3684,28 @@ async function resolveTask(client, identifier, scopeSession) {
3452
3684
  }
3453
3685
  async function createTaskCore(input2) {
3454
3686
  const client = getClient();
3455
- const id = crypto3.randomUUID();
3687
+ const id = crypto4.randomUUID();
3456
3688
  const now = (/* @__PURE__ */ new Date()).toISOString();
3457
3689
  const slug = slugify(input2.title);
3458
3690
  let earlySessionScope = null;
3691
+ let scopeMismatchWarning;
3459
3692
  try {
3460
3693
  const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3461
- earlySessionScope = resolveExeSession2();
3694
+ const resolved = resolveExeSession2();
3695
+ if (resolved && input2.projectName) {
3696
+ const { getSessionProject: getSessionProject2 } = await Promise.resolve().then(() => (init_session_scope(), session_scope_exports));
3697
+ const sessionProject = getSessionProject2(resolved);
3698
+ if (sessionProject && sessionProject !== input2.projectName) {
3699
+ scopeMismatchWarning = `session/project mismatch: session "${resolved}" owns "${sessionProject}" but task targets "${input2.projectName}". Routed to default scope.`;
3700
+ process.stderr.write(`[create_task] ${scopeMismatchWarning}
3701
+ `);
3702
+ earlySessionScope = null;
3703
+ } else {
3704
+ earlySessionScope = resolved;
3705
+ }
3706
+ } else {
3707
+ earlySessionScope = resolved;
3708
+ }
3462
3709
  } catch {
3463
3710
  }
3464
3711
  const scope = earlySessionScope ?? "default";
@@ -3509,10 +3756,14 @@ async function createTaskCore(input2) {
3509
3756
  ${laneWarning}` : laneWarning;
3510
3757
  }
3511
3758
  }
3759
+ if (scopeMismatchWarning) {
3760
+ warning = warning ? `${warning}
3761
+ ${scopeMismatchWarning}` : scopeMismatchWarning;
3762
+ }
3512
3763
  if (input2.baseDir) {
3513
3764
  try {
3514
- await mkdir3(path12.join(input2.baseDir, "exe", "output"), { recursive: true });
3515
- await mkdir3(path12.join(input2.baseDir, "exe", "research"), { recursive: true });
3765
+ await mkdir3(path14.join(input2.baseDir, "exe", "output"), { recursive: true });
3766
+ await mkdir3(path14.join(input2.baseDir, "exe", "research"), { recursive: true });
3516
3767
  await ensureArchitectureDoc(input2.baseDir, input2.projectName);
3517
3768
  await ensureGitignoreExe(input2.baseDir);
3518
3769
  } catch {
@@ -3548,13 +3799,19 @@ ${laneWarning}` : laneWarning;
3548
3799
  });
3549
3800
  if (input2.baseDir) {
3550
3801
  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 });
3802
+ const EXE_OS_DIR = path14.join(os9.homedir(), ".exe-os");
3803
+ const mdPath = path14.join(EXE_OS_DIR, taskFile);
3804
+ const mdDir = path14.dirname(mdPath);
3805
+ if (!existsSync12(mdDir)) await mkdir3(mdDir, { recursive: true });
3555
3806
  const reviewer = input2.reviewer ?? input2.assignedBy;
3556
3807
  const mdContent = `# ${input2.title}
3557
3808
 
3809
+ ## MANDATORY: When done
3810
+
3811
+ You MUST call update_task with status "done" and a result summary when finished.
3812
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3813
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
3814
+
3558
3815
  **ID:** ${id}
3559
3816
  **Status:** ${initialStatus}
3560
3817
  **Priority:** ${input2.priority}
@@ -3568,12 +3825,6 @@ ${laneWarning}` : laneWarning;
3568
3825
  ## Context
3569
3826
 
3570
3827
  ${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
3828
  `;
3578
3829
  await writeFile3(mdPath, mdContent, "utf-8");
3579
3830
  } catch (err) {
@@ -3655,14 +3906,14 @@ function isTmuxSessionAlive(identifier) {
3655
3906
  if (!identifier || identifier === "unknown") return true;
3656
3907
  try {
3657
3908
  if (identifier.startsWith("%")) {
3658
- const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
3909
+ const output = execSync6("tmux list-panes -a -F '#{pane_id}'", {
3659
3910
  timeout: 2e3,
3660
3911
  encoding: "utf8",
3661
3912
  stdio: ["pipe", "pipe", "pipe"]
3662
3913
  });
3663
3914
  return output.split("\n").some((l) => l.trim() === identifier);
3664
3915
  } else {
3665
- execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
3916
+ execSync6(`tmux has-session -t ${JSON.stringify(identifier)}`, {
3666
3917
  timeout: 2e3,
3667
3918
  stdio: ["pipe", "pipe", "pipe"]
3668
3919
  });
@@ -3671,7 +3922,7 @@ function isTmuxSessionAlive(identifier) {
3671
3922
  } catch {
3672
3923
  if (identifier.startsWith("%")) return true;
3673
3924
  try {
3674
- execSync5("tmux list-sessions", {
3925
+ execSync6("tmux list-sessions", {
3675
3926
  timeout: 2e3,
3676
3927
  stdio: ["pipe", "pipe", "pipe"]
3677
3928
  });
@@ -3686,12 +3937,12 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
3686
3937
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
3687
3938
  try {
3688
3939
  const since = new Date(taskCreatedAt).toISOString();
3689
- const branch = execSync5(
3940
+ const branch = execSync6(
3690
3941
  "git rev-parse --abbrev-ref HEAD 2>/dev/null",
3691
3942
  { encoding: "utf8", timeout: 3e3 }
3692
3943
  ).trim();
3693
3944
  const branchArg = branch && branch !== "HEAD" ? branch : "";
3694
- const commitCount = execSync5(
3945
+ const commitCount = execSync6(
3695
3946
  `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
3696
3947
  { encoding: "utf8", timeout: 5e3 }
3697
3948
  ).trim();
@@ -3822,7 +4073,7 @@ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
3822
4073
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
3823
4074
  } catch {
3824
4075
  }
3825
- if (input2.status === "done" || input2.status === "cancelled") {
4076
+ if (input2.status === "done" || input2.status === "cancelled" || input2.status === "closed") {
3826
4077
  try {
3827
4078
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
3828
4079
  clearQueueForAgent2(String(row.assigned_to));
@@ -3851,9 +4102,9 @@ async function deleteTaskCore(taskId, _baseDir) {
3851
4102
  return { taskFile, assignedTo, assignedBy, taskSlug };
3852
4103
  }
3853
4104
  async function ensureArchitectureDoc(baseDir, projectName) {
3854
- const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
4105
+ const archPath = path14.join(baseDir, "exe", "ARCHITECTURE.md");
3855
4106
  try {
3856
- if (existsSync10(archPath)) return;
4107
+ if (existsSync12(archPath)) return;
3857
4108
  const template = [
3858
4109
  `# ${projectName} \u2014 System Architecture`,
3859
4110
  "",
@@ -3886,10 +4137,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
3886
4137
  }
3887
4138
  }
3888
4139
  async function ensureGitignoreExe(baseDir) {
3889
- const gitignorePath = path12.join(baseDir, ".gitignore");
4140
+ const gitignorePath = path14.join(baseDir, ".gitignore");
3890
4141
  try {
3891
- if (existsSync10(gitignorePath)) {
3892
- const content = readFileSync11(gitignorePath, "utf-8");
4142
+ if (existsSync12(gitignorePath)) {
4143
+ const content = readFileSync12(gitignorePath, "utf-8");
3893
4144
  if (/^\/?exe\/?$/m.test(content)) return;
3894
4145
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3895
4146
  } else {
@@ -3920,58 +4171,42 @@ var init_tasks_crud = __esm({
3920
4171
  });
3921
4172
 
3922
4173
  // src/lib/tasks-review.ts
3923
- import path13 from "path";
3924
- import { existsSync as existsSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
4174
+ import path15 from "path";
4175
+ import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
3925
4176
  async function countPendingReviews(sessionScope) {
3926
4177
  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
- }
4178
+ const scope = strictSessionScopeFilter(
4179
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
4180
+ );
3934
4181
  const result = await client.execute({
3935
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3936
- args: []
4182
+ sql: `SELECT COUNT(*) as cnt FROM tasks
4183
+ WHERE status = 'needs_review'${scope.sql}`,
4184
+ args: [...scope.args]
3937
4185
  });
3938
4186
  return Number(result.rows[0]?.cnt) || 0;
3939
4187
  }
3940
4188
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3941
4189
  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
- }
4190
+ const scope = strictSessionScopeFilter(
4191
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
4192
+ );
3951
4193
  const result = await client.execute({
3952
4194
  sql: `SELECT COUNT(*) as cnt FROM tasks
3953
- WHERE status = 'needs_review' AND updated_at > ?`,
3954
- args: [sinceIso]
4195
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
4196
+ args: [sinceIso, ...scope.args]
3955
4197
  });
3956
4198
  return Number(result.rows[0]?.cnt) || 0;
3957
4199
  }
3958
4200
  async function listPendingReviews(limit, sessionScope) {
3959
4201
  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
- }
4202
+ const scope = strictSessionScopeFilter(
4203
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
4204
+ );
3970
4205
  const result = await client.execute({
3971
4206
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3972
- WHERE status = 'needs_review'
4207
+ WHERE status = 'needs_review'${scope.sql}
3973
4208
  ORDER BY updated_at ASC LIMIT ?`,
3974
- args: [limit]
4209
+ args: [...scope.args, limit]
3975
4210
  });
3976
4211
  return result.rows;
3977
4212
  }
@@ -3983,7 +4218,7 @@ async function cleanupOrphanedReviews() {
3983
4218
  WHERE status IN ('open', 'needs_review', 'in_progress')
3984
4219
  AND assigned_by = 'system'
3985
4220
  AND title LIKE 'Review:%'
3986
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
4221
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
3987
4222
  args: [now]
3988
4223
  });
3989
4224
  const r1b = await client.execute({
@@ -4102,11 +4337,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4102
4337
  );
4103
4338
  }
4104
4339
  try {
4105
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
4106
- if (existsSync11(cacheDir)) {
4340
+ const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
4341
+ if (existsSync13(cacheDir)) {
4107
4342
  for (const f of readdirSync3(cacheDir)) {
4108
4343
  if (f.startsWith("review-notified-")) {
4109
- unlinkSync5(path13.join(cacheDir, f));
4344
+ unlinkSync5(path15.join(cacheDir, f));
4110
4345
  }
4111
4346
  }
4112
4347
  }
@@ -4123,11 +4358,12 @@ var init_tasks_review = __esm({
4123
4358
  init_tmux_routing();
4124
4359
  init_session_key();
4125
4360
  init_state_bus();
4361
+ init_task_scope();
4126
4362
  }
4127
4363
  });
4128
4364
 
4129
4365
  // src/lib/tasks-chain.ts
4130
- import path14 from "path";
4366
+ import path16 from "path";
4131
4367
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
4132
4368
  async function cascadeUnblock(taskId, baseDir, now) {
4133
4369
  const client = getClient();
@@ -4144,7 +4380,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
4144
4380
  });
4145
4381
  for (const ur of unblockedRows.rows) {
4146
4382
  try {
4147
- const ubFile = path14.join(baseDir, String(ur.task_file));
4383
+ const ubFile = path16.join(baseDir, String(ur.task_file));
4148
4384
  let ubContent = await readFile3(ubFile, "utf-8");
4149
4385
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
4150
4386
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -4179,7 +4415,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
4179
4415
  const scScope = sessionScopeFilter();
4180
4416
  const remaining = await client.execute({
4181
4417
  sql: `SELECT COUNT(*) as cnt FROM tasks
4182
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
4418
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
4183
4419
  args: [parentTaskId, ...scScope.args]
4184
4420
  });
4185
4421
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -4211,110 +4447,6 @@ var init_tasks_chain = __esm({
4211
4447
  }
4212
4448
  });
4213
4449
 
4214
- // src/lib/project-name.ts
4215
- import { execSync as execSync6 } from "child_process";
4216
- import path15 from "path";
4217
- function getProjectName(cwd) {
4218
- const dir = cwd ?? process.cwd();
4219
- if (_cached2 && _cachedCwd === dir) return _cached2;
4220
- try {
4221
- let repoRoot;
4222
- try {
4223
- const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
4224
- cwd: dir,
4225
- encoding: "utf8",
4226
- timeout: 2e3,
4227
- stdio: ["pipe", "pipe", "pipe"]
4228
- }).trim();
4229
- repoRoot = path15.dirname(gitCommonDir);
4230
- } catch {
4231
- repoRoot = execSync6("git rev-parse --show-toplevel", {
4232
- cwd: dir,
4233
- encoding: "utf8",
4234
- timeout: 2e3,
4235
- stdio: ["pipe", "pipe", "pipe"]
4236
- }).trim();
4237
- }
4238
- _cached2 = path15.basename(repoRoot);
4239
- _cachedCwd = dir;
4240
- return _cached2;
4241
- } catch {
4242
- _cached2 = path15.basename(dir);
4243
- _cachedCwd = dir;
4244
- return _cached2;
4245
- }
4246
- }
4247
- var _cached2, _cachedCwd;
4248
- var init_project_name = __esm({
4249
- "src/lib/project-name.ts"() {
4250
- "use strict";
4251
- _cached2 = null;
4252
- _cachedCwd = null;
4253
- }
4254
- });
4255
-
4256
- // src/lib/session-scope.ts
4257
- var session_scope_exports = {};
4258
- __export(session_scope_exports, {
4259
- assertSessionScope: () => assertSessionScope,
4260
- findSessionForProject: () => findSessionForProject,
4261
- getSessionProject: () => getSessionProject
4262
- });
4263
- function getSessionProject(sessionName) {
4264
- const sessions = listSessions();
4265
- const entry = sessions.find((s) => s.windowName === sessionName);
4266
- if (!entry) return null;
4267
- const parts = entry.projectDir.split("/").filter(Boolean);
4268
- return parts[parts.length - 1] ?? null;
4269
- }
4270
- function findSessionForProject(projectName) {
4271
- const sessions = listSessions();
4272
- for (const s of sessions) {
4273
- const proj = s.projectDir.split("/").filter(Boolean).pop();
4274
- if (proj === projectName && isCoordinatorName(s.agentId)) return s;
4275
- }
4276
- return null;
4277
- }
4278
- function assertSessionScope(actionType, targetProject) {
4279
- try {
4280
- const currentProject = getProjectName();
4281
- const exeSession = resolveExeSession();
4282
- if (!exeSession) {
4283
- return { allowed: true, reason: "no_session" };
4284
- }
4285
- if (currentProject === targetProject) {
4286
- return {
4287
- allowed: true,
4288
- reason: "same_session",
4289
- currentProject,
4290
- targetProject
4291
- };
4292
- }
4293
- process.stderr.write(
4294
- `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
4295
- `
4296
- );
4297
- return {
4298
- allowed: false,
4299
- reason: "cross_session_denied",
4300
- currentProject,
4301
- targetProject,
4302
- targetSession: findSessionForProject(targetProject)?.windowName
4303
- };
4304
- } catch {
4305
- return { allowed: true, reason: "no_session" };
4306
- }
4307
- }
4308
- var init_session_scope = __esm({
4309
- "src/lib/session-scope.ts"() {
4310
- "use strict";
4311
- init_session_registry();
4312
- init_project_name();
4313
- init_tmux_routing();
4314
- init_employees();
4315
- }
4316
- });
4317
-
4318
4450
  // src/lib/tasks-notify.ts
4319
4451
  async function dispatchTaskToEmployee(input2) {
4320
4452
  if (isCoordinatorName(input2.assignedTo)) return { dispatched: "skipped" };
@@ -4382,10 +4514,10 @@ var init_tasks_notify = __esm({
4382
4514
  });
4383
4515
 
4384
4516
  // src/lib/behaviors.ts
4385
- import crypto4 from "crypto";
4517
+ import crypto5 from "crypto";
4386
4518
  async function storeBehavior(opts) {
4387
4519
  const client = getClient();
4388
- const id = crypto4.randomUUID();
4520
+ const id = crypto5.randomUUID();
4389
4521
  const now = (/* @__PURE__ */ new Date()).toISOString();
4390
4522
  await client.execute({
4391
4523
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -4414,7 +4546,7 @@ __export(skill_learning_exports, {
4414
4546
  storeTrajectory: () => storeTrajectory,
4415
4547
  sweepTrajectories: () => sweepTrajectories
4416
4548
  });
4417
- import crypto5 from "crypto";
4549
+ import crypto6 from "crypto";
4418
4550
  async function extractTrajectory(taskId, agentId) {
4419
4551
  const client = getClient();
4420
4552
  const result = await client.execute({
@@ -4443,11 +4575,11 @@ async function extractTrajectory(taskId, agentId) {
4443
4575
  return signature;
4444
4576
  }
4445
4577
  function hashSignature(signature) {
4446
- return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4578
+ return crypto6.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4447
4579
  }
4448
4580
  async function storeTrajectory(opts) {
4449
4581
  const client = getClient();
4450
- const id = crypto5.randomUUID();
4582
+ const id = crypto6.randomUUID();
4451
4583
  const now = (/* @__PURE__ */ new Date()).toISOString();
4452
4584
  const signatureHash = hashSignature(opts.signature);
4453
4585
  await client.execute({
@@ -4712,8 +4844,8 @@ __export(tasks_exports, {
4712
4844
  updateTaskStatus: () => updateTaskStatus,
4713
4845
  writeCheckpoint: () => writeCheckpoint
4714
4846
  });
4715
- import path16 from "path";
4716
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync6 } from "fs";
4847
+ import path17 from "path";
4848
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync6 } from "fs";
4717
4849
  async function createTask(input2) {
4718
4850
  const result = await createTaskCore(input2);
4719
4851
  if (!input2.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -4732,12 +4864,12 @@ async function updateTask(input2) {
4732
4864
  const { row, taskFile, now, taskId } = await updateTaskStatus(input2);
4733
4865
  try {
4734
4866
  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`);
4867
+ const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
4868
+ const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
4737
4869
  if (input2.status === "in_progress") {
4738
4870
  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") {
4871
+ writeFileSync8(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4872
+ } else if (input2.status === "done" || input2.status === "blocked" || input2.status === "cancelled" || input2.status === "closed") {
4741
4873
  try {
4742
4874
  unlinkSync6(cachePath);
4743
4875
  } catch {
@@ -4745,10 +4877,10 @@ async function updateTask(input2) {
4745
4877
  }
4746
4878
  } catch {
4747
4879
  }
4748
- if (input2.status === "done") {
4880
+ if (input2.status === "done" || input2.status === "closed") {
4749
4881
  await cleanupReviewFile(row, taskFile, input2.baseDir);
4750
4882
  }
4751
- if (input2.status === "done" || input2.status === "cancelled") {
4883
+ if (input2.status === "done" || input2.status === "cancelled" || input2.status === "closed") {
4752
4884
  try {
4753
4885
  const client = getClient();
4754
4886
  const taskTitle = String(row.title);
@@ -4764,7 +4896,7 @@ async function updateTask(input2) {
4764
4896
  if (!isCoordinatorName(assignedAgent)) {
4765
4897
  try {
4766
4898
  const draftClient = getClient();
4767
- if (input2.status === "done") {
4899
+ if (input2.status === "done" || input2.status === "closed") {
4768
4900
  await draftClient.execute({
4769
4901
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
4770
4902
  args: [assignedAgent]
@@ -4781,7 +4913,7 @@ async function updateTask(input2) {
4781
4913
  try {
4782
4914
  const client = getClient();
4783
4915
  const cascaded = await client.execute({
4784
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
4916
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
4785
4917
  WHERE parent_task_id = ? AND status = 'needs_review'`,
4786
4918
  args: [now, taskId]
4787
4919
  });
@@ -4794,14 +4926,14 @@ async function updateTask(input2) {
4794
4926
  } catch {
4795
4927
  }
4796
4928
  }
4797
- const isTerminal = input2.status === "done" || input2.status === "needs_review";
4929
+ const isTerminal = input2.status === "done" || input2.status === "needs_review" || input2.status === "closed";
4798
4930
  if (isTerminal) {
4799
4931
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
4800
4932
  if (!isCoordinator) {
4801
4933
  notifyTaskDone();
4802
4934
  }
4803
4935
  await markTaskNotificationsRead(taskFile);
4804
- if (input2.status === "done") {
4936
+ if (input2.status === "done" || input2.status === "closed") {
4805
4937
  try {
4806
4938
  await cascadeUnblock(taskId, input2.baseDir, now);
4807
4939
  } catch {
@@ -4821,7 +4953,7 @@ async function updateTask(input2) {
4821
4953
  }
4822
4954
  }
4823
4955
  }
4824
- if (input2.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4956
+ if ((input2.status === "done" || input2.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4825
4957
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4826
4958
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4827
4959
  taskId,
@@ -5193,6 +5325,7 @@ __export(tmux_routing_exports, {
5193
5325
  isEmployeeAlive: () => isEmployeeAlive,
5194
5326
  isExeSession: () => isExeSession,
5195
5327
  isSessionBusy: () => isSessionBusy,
5328
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
5196
5329
  notifyParentExe: () => notifyParentExe,
5197
5330
  parseParentExe: () => parseParentExe,
5198
5331
  registerParentExe: () => registerParentExe,
@@ -5203,13 +5336,13 @@ __export(tmux_routing_exports, {
5203
5336
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
5204
5337
  });
5205
5338
  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";
5339
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, existsSync as existsSync14, appendFileSync, readdirSync as readdirSync4 } from "fs";
5340
+ import path18 from "path";
5341
+ import os10 from "os";
5209
5342
  import { fileURLToPath as fileURLToPath2 } from "url";
5210
5343
  import { unlinkSync as unlinkSync7 } from "fs";
5211
5344
  function spawnLockPath(sessionName) {
5212
- return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5345
+ return path18.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5213
5346
  }
5214
5347
  function isProcessAlive(pid) {
5215
5348
  try {
@@ -5220,13 +5353,13 @@ function isProcessAlive(pid) {
5220
5353
  }
5221
5354
  }
5222
5355
  function acquireSpawnLock2(sessionName) {
5223
- if (!existsSync12(SPAWN_LOCK_DIR)) {
5356
+ if (!existsSync14(SPAWN_LOCK_DIR)) {
5224
5357
  mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
5225
5358
  }
5226
5359
  const lockFile = spawnLockPath(sessionName);
5227
- if (existsSync12(lockFile)) {
5360
+ if (existsSync14(lockFile)) {
5228
5361
  try {
5229
- const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
5362
+ const lock = JSON.parse(readFileSync13(lockFile, "utf8"));
5230
5363
  const age = Date.now() - lock.timestamp;
5231
5364
  if (isProcessAlive(lock.pid) && age < 6e4) {
5232
5365
  return false;
@@ -5234,7 +5367,7 @@ function acquireSpawnLock2(sessionName) {
5234
5367
  } catch {
5235
5368
  }
5236
5369
  }
5237
- writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5370
+ writeFileSync9(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5238
5371
  return true;
5239
5372
  }
5240
5373
  function releaseSpawnLock2(sessionName) {
@@ -5246,13 +5379,13 @@ function releaseSpawnLock2(sessionName) {
5246
5379
  function resolveBehaviorsExporterScript() {
5247
5380
  try {
5248
5381
  const thisFile = fileURLToPath2(import.meta.url);
5249
- const scriptPath = path17.join(
5250
- path17.dirname(thisFile),
5382
+ const scriptPath = path18.join(
5383
+ path18.dirname(thisFile),
5251
5384
  "..",
5252
5385
  "bin",
5253
5386
  "exe-export-behaviors.js"
5254
5387
  );
5255
- return existsSync12(scriptPath) ? scriptPath : null;
5388
+ return existsSync14(scriptPath) ? scriptPath : null;
5256
5389
  } catch {
5257
5390
  return null;
5258
5391
  }
@@ -5318,12 +5451,12 @@ function extractRootExe(name) {
5318
5451
  return parts.length > 0 ? parts[parts.length - 1] : null;
5319
5452
  }
5320
5453
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5321
- if (!existsSync12(SESSION_CACHE)) {
5454
+ if (!existsSync14(SESSION_CACHE)) {
5322
5455
  mkdirSync7(SESSION_CACHE, { recursive: true });
5323
5456
  }
5324
5457
  const rootExe = extractRootExe(parentExe) ?? parentExe;
5325
- const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5326
- writeFileSync8(filePath, JSON.stringify({
5458
+ const filePath = path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5459
+ writeFileSync9(filePath, JSON.stringify({
5327
5460
  parentExe: rootExe,
5328
5461
  dispatchedBy: dispatchedBy || rootExe,
5329
5462
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5331,7 +5464,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5331
5464
  }
5332
5465
  function getParentExe(sessionKey) {
5333
5466
  try {
5334
- const data = JSON.parse(readFileSync12(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5467
+ const data = JSON.parse(readFileSync13(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5335
5468
  return data.parentExe || null;
5336
5469
  } catch {
5337
5470
  return null;
@@ -5339,8 +5472,8 @@ function getParentExe(sessionKey) {
5339
5472
  }
5340
5473
  function getDispatchedBy(sessionKey) {
5341
5474
  try {
5342
- const data = JSON.parse(readFileSync12(
5343
- path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5475
+ const data = JSON.parse(readFileSync13(
5476
+ path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5344
5477
  "utf8"
5345
5478
  ));
5346
5479
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -5410,8 +5543,8 @@ async function verifyPaneAtCapacity(sessionName) {
5410
5543
  }
5411
5544
  function readDebounceState() {
5412
5545
  try {
5413
- if (!existsSync12(DEBOUNCE_FILE)) return {};
5414
- const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
5546
+ if (!existsSync14(DEBOUNCE_FILE)) return {};
5547
+ const raw = JSON.parse(readFileSync13(DEBOUNCE_FILE, "utf8"));
5415
5548
  const state = {};
5416
5549
  for (const [key, val] of Object.entries(raw)) {
5417
5550
  if (typeof val === "number") {
@@ -5427,8 +5560,8 @@ function readDebounceState() {
5427
5560
  }
5428
5561
  function writeDebounceState(state) {
5429
5562
  try {
5430
- if (!existsSync12(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
5431
- writeFileSync8(DEBOUNCE_FILE, JSON.stringify(state));
5563
+ if (!existsSync14(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
5564
+ writeFileSync9(DEBOUNCE_FILE, JSON.stringify(state));
5432
5565
  } catch {
5433
5566
  }
5434
5567
  }
@@ -5526,8 +5659,8 @@ function sendIntercom(targetSession) {
5526
5659
  try {
5527
5660
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5528
5661
  const agent = baseAgentName(rawAgent);
5529
- const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5530
- if (existsSync12(markerPath)) {
5662
+ const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
5663
+ if (existsSync14(markerPath)) {
5531
5664
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5532
5665
  return "debounced";
5533
5666
  }
@@ -5536,8 +5669,8 @@ function sendIntercom(targetSession) {
5536
5669
  try {
5537
5670
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5538
5671
  const agent = baseAgentName(rawAgent);
5539
- const taskDir = path17.join(process.cwd(), "exe", agent);
5540
- if (existsSync12(taskDir)) {
5672
+ const taskDir = path18.join(process.cwd(), "exe", agent);
5673
+ if (existsSync14(taskDir)) {
5541
5674
  const files = readdirSync4(taskDir).filter(
5542
5675
  (f) => f.endsWith(".md") && f !== "DONE.txt"
5543
5676
  );
@@ -5597,6 +5730,21 @@ function notifyParentExe(sessionKey) {
5597
5730
  }
5598
5731
  return true;
5599
5732
  }
5733
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
5734
+ const transport = getTransport();
5735
+ try {
5736
+ const sessions = transport.listSessions();
5737
+ if (!sessions.includes(coordinatorSession)) return false;
5738
+ execSync7(
5739
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5740
+ { timeout: 3e3 }
5741
+ );
5742
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
5743
+ return true;
5744
+ } catch {
5745
+ return false;
5746
+ }
5747
+ }
5600
5748
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
5601
5749
  if (isCoordinatorName(employeeName)) {
5602
5750
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -5670,26 +5818,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5670
5818
  const transport = getTransport();
5671
5819
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
5672
5820
  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)) {
5821
+ const logDir = path18.join(os10.homedir(), ".exe-os", "session-logs");
5822
+ const logFile = path18.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5823
+ if (!existsSync14(logDir)) {
5676
5824
  mkdirSync7(logDir, { recursive: true });
5677
5825
  }
5678
5826
  transport.kill(sessionName);
5679
5827
  let cleanupSuffix = "";
5680
5828
  try {
5681
5829
  const thisFile = fileURLToPath2(import.meta.url);
5682
- const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5683
- if (existsSync12(cleanupScript)) {
5830
+ const cleanupScript = path18.join(path18.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5831
+ if (existsSync14(cleanupScript)) {
5684
5832
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
5685
5833
  }
5686
5834
  } catch {
5687
5835
  }
5688
5836
  try {
5689
- const claudeJsonPath = path17.join(os9.homedir(), ".claude.json");
5837
+ const claudeJsonPath = path18.join(os10.homedir(), ".claude.json");
5690
5838
  let claudeJson = {};
5691
5839
  try {
5692
- claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
5840
+ claudeJson = JSON.parse(readFileSync13(claudeJsonPath, "utf8"));
5693
5841
  } catch {
5694
5842
  }
5695
5843
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -5697,17 +5845,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5697
5845
  const trustDir = opts?.cwd ?? projectDir;
5698
5846
  if (!projects[trustDir]) projects[trustDir] = {};
5699
5847
  projects[trustDir].hasTrustDialogAccepted = true;
5700
- writeFileSync8(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5848
+ writeFileSync9(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5701
5849
  } catch {
5702
5850
  }
5703
5851
  try {
5704
- const settingsDir = path17.join(os9.homedir(), ".claude", "projects");
5852
+ const settingsDir = path18.join(os10.homedir(), ".claude", "projects");
5705
5853
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
5706
- const projSettingsDir = path17.join(settingsDir, normalizedKey);
5707
- const settingsPath = path17.join(projSettingsDir, "settings.json");
5854
+ const projSettingsDir = path18.join(settingsDir, normalizedKey);
5855
+ const settingsPath = path18.join(projSettingsDir, "settings.json");
5708
5856
  let settings = {};
5709
5857
  try {
5710
- settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
5858
+ settings = JSON.parse(readFileSync13(settingsPath, "utf8"));
5711
5859
  } catch {
5712
5860
  }
5713
5861
  const perms = settings.permissions ?? {};
@@ -5736,7 +5884,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5736
5884
  perms.allow = allow;
5737
5885
  settings.permissions = perms;
5738
5886
  mkdirSync7(projSettingsDir, { recursive: true });
5739
- writeFileSync8(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5887
+ writeFileSync9(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5740
5888
  }
5741
5889
  } catch {
5742
5890
  }
@@ -5751,8 +5899,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5751
5899
  let behaviorsFlag = "";
5752
5900
  let legacyFallbackWarned = false;
5753
5901
  if (!useExeAgent && !useBinSymlink) {
5754
- const identityPath = path17.join(
5755
- os9.homedir(),
5902
+ const identityPath = path18.join(
5903
+ os10.homedir(),
5756
5904
  ".exe-os",
5757
5905
  "identity",
5758
5906
  `${employeeName}.md`
@@ -5761,13 +5909,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5761
5909
  const hasAgentFlag = claudeSupportsAgentFlag();
5762
5910
  if (hasAgentFlag) {
5763
5911
  identityFlag = ` --agent ${employeeName}`;
5764
- } else if (existsSync12(identityPath)) {
5912
+ } else if (existsSync14(identityPath)) {
5765
5913
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
5766
5914
  legacyFallbackWarned = true;
5767
5915
  }
5768
5916
  const behaviorsFile = exportBehaviorsSync(
5769
5917
  employeeName,
5770
- path17.basename(spawnCwd),
5918
+ path18.basename(spawnCwd),
5771
5919
  sessionName
5772
5920
  );
5773
5921
  if (behaviorsFile) {
@@ -5782,16 +5930,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5782
5930
  }
5783
5931
  let sessionContextFlag = "";
5784
5932
  try {
5785
- const ctxDir = path17.join(os9.homedir(), ".exe-os", "session-cache");
5933
+ const ctxDir = path18.join(os10.homedir(), ".exe-os", "session-cache");
5786
5934
  mkdirSync7(ctxDir, { recursive: true });
5787
- const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
5935
+ const ctxFile = path18.join(ctxDir, `session-context-${sessionName}.md`);
5788
5936
  const ctxContent = [
5789
5937
  `## Session Context`,
5790
5938
  `You are running in tmux session: ${sessionName}.`,
5791
5939
  `Your parent coordinator session is ${exeSession}.`,
5792
5940
  `Your employees (if any) use the -${exeSession} suffix.`
5793
5941
  ].join("\n");
5794
- writeFileSync8(ctxFile, ctxContent);
5942
+ writeFileSync9(ctxFile, ctxContent);
5795
5943
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5796
5944
  } catch {
5797
5945
  }
@@ -5868,8 +6016,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5868
6016
  transport.pipeLog(sessionName, logFile);
5869
6017
  try {
5870
6018
  const mySession = getMySession();
5871
- const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5872
- writeFileSync8(dispatchInfo, JSON.stringify({
6019
+ const dispatchInfo = path18.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
6020
+ writeFileSync9(dispatchInfo, JSON.stringify({
5873
6021
  dispatchedBy: mySession,
5874
6022
  rootExe: exeSession,
5875
6023
  provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
@@ -5943,15 +6091,15 @@ var init_tmux_routing = __esm({
5943
6091
  init_intercom_queue();
5944
6092
  init_plan_limits();
5945
6093
  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");
6094
+ SPAWN_LOCK_DIR = path18.join(os10.homedir(), ".exe-os", "spawn-locks");
6095
+ SESSION_CACHE = path18.join(os10.homedir(), ".exe-os", "session-cache");
5948
6096
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5949
6097
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
5950
6098
  VERIFY_PANE_LINES = 200;
5951
6099
  INTERCOM_DEBOUNCE_MS = 3e4;
5952
6100
  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");
6101
+ INTERCOM_LOG2 = path18.join(os10.homedir(), ".exe-os", "intercom.log");
6102
+ DEBOUNCE_FILE = path18.join(SESSION_CACHE, "intercom-debounce.json");
5955
6103
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5956
6104
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
5957
6105
  }
@@ -5974,6 +6122,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
5974
6122
  args: [scope]
5975
6123
  };
5976
6124
  }
6125
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
6126
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
6127
+ if (!scope) return { sql: "", args: [] };
6128
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
6129
+ return {
6130
+ sql: ` AND ${col} = ?`,
6131
+ args: [scope]
6132
+ };
6133
+ }
5977
6134
  var init_task_scope = __esm({
5978
6135
  "src/lib/task-scope.ts"() {
5979
6136
  "use strict";
@@ -5992,14 +6149,14 @@ var init_memory = __esm({
5992
6149
 
5993
6150
  // src/lib/keychain.ts
5994
6151
  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";
6152
+ import { existsSync as existsSync15 } from "fs";
6153
+ import path19 from "path";
6154
+ import os11 from "os";
5998
6155
  function getKeyDir() {
5999
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path18.join(os10.homedir(), ".exe-os");
6156
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path19.join(os11.homedir(), ".exe-os");
6000
6157
  }
6001
6158
  function getKeyPath() {
6002
- return path18.join(getKeyDir(), "master.key");
6159
+ return path19.join(getKeyDir(), "master.key");
6003
6160
  }
6004
6161
  async function tryKeytar() {
6005
6162
  try {
@@ -6020,9 +6177,9 @@ async function getMasterKey() {
6020
6177
  }
6021
6178
  }
6022
6179
  const keyPath = getKeyPath();
6023
- if (!existsSync13(keyPath)) {
6180
+ if (!existsSync15(keyPath)) {
6024
6181
  process.stderr.write(
6025
- `[keychain] Key not found at ${keyPath} (HOME=${os10.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
6182
+ `[keychain] Key not found at ${keyPath} (HOME=${os11.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
6026
6183
  `
6027
6184
  );
6028
6185
  return null;
@@ -6052,6 +6209,7 @@ var shard_manager_exports = {};
6052
6209
  __export(shard_manager_exports, {
6053
6210
  disposeShards: () => disposeShards,
6054
6211
  ensureShardSchema: () => ensureShardSchema,
6212
+ getOpenShardCount: () => getOpenShardCount,
6055
6213
  getReadyShardClient: () => getReadyShardClient,
6056
6214
  getShardClient: () => getShardClient,
6057
6215
  getShardsDir: () => getShardsDir,
@@ -6060,15 +6218,18 @@ __export(shard_manager_exports, {
6060
6218
  listShards: () => listShards,
6061
6219
  shardExists: () => shardExists
6062
6220
  });
6063
- import path19 from "path";
6064
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync5 } from "fs";
6221
+ import path20 from "path";
6222
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync5 } from "fs";
6065
6223
  import { createClient as createClient2 } from "@libsql/client";
6066
6224
  function initShardManager(encryptionKey) {
6067
6225
  _encryptionKey = encryptionKey;
6068
- if (!existsSync14(SHARDS_DIR)) {
6226
+ if (!existsSync16(SHARDS_DIR)) {
6069
6227
  mkdirSync8(SHARDS_DIR, { recursive: true });
6070
6228
  }
6071
6229
  _shardingEnabled = true;
6230
+ if (_evictionTimer) clearInterval(_evictionTimer);
6231
+ _evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
6232
+ _evictionTimer.unref();
6072
6233
  }
6073
6234
  function isShardingEnabled() {
6074
6235
  return _shardingEnabled;
@@ -6085,21 +6246,28 @@ function getShardClient(projectName) {
6085
6246
  throw new Error(`Invalid project name for shard: "${projectName}"`);
6086
6247
  }
6087
6248
  const cached = _shards.get(safeName);
6088
- if (cached) return cached;
6089
- const dbPath = path19.join(SHARDS_DIR, `${safeName}.db`);
6249
+ if (cached) {
6250
+ _shardLastAccess.set(safeName, Date.now());
6251
+ return cached;
6252
+ }
6253
+ while (_shards.size >= MAX_OPEN_SHARDS) {
6254
+ evictLRU();
6255
+ }
6256
+ const dbPath = path20.join(SHARDS_DIR, `${safeName}.db`);
6090
6257
  const client = createClient2({
6091
6258
  url: `file:${dbPath}`,
6092
6259
  encryptionKey: _encryptionKey
6093
6260
  });
6094
6261
  _shards.set(safeName, client);
6262
+ _shardLastAccess.set(safeName, Date.now());
6095
6263
  return client;
6096
6264
  }
6097
6265
  function shardExists(projectName) {
6098
6266
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
6099
- return existsSync14(path19.join(SHARDS_DIR, `${safeName}.db`));
6267
+ return existsSync16(path20.join(SHARDS_DIR, `${safeName}.db`));
6100
6268
  }
6101
6269
  function listShards() {
6102
- if (!existsSync14(SHARDS_DIR)) return [];
6270
+ if (!existsSync16(SHARDS_DIR)) return [];
6103
6271
  return readdirSync5(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
6104
6272
  }
6105
6273
  async function ensureShardSchema(client) {
@@ -6151,6 +6319,8 @@ async function ensureShardSchema(client) {
6151
6319
  for (const col of [
6152
6320
  "ALTER TABLE memories ADD COLUMN task_id TEXT",
6153
6321
  "ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
6322
+ "ALTER TABLE memories ADD COLUMN author_device_id TEXT",
6323
+ "ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
6154
6324
  "ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
6155
6325
  "ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
6156
6326
  "ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
@@ -6288,21 +6458,69 @@ async function getReadyShardClient(projectName) {
6288
6458
  await ensureShardSchema(client);
6289
6459
  return client;
6290
6460
  }
6461
+ function evictLRU() {
6462
+ let oldest = null;
6463
+ let oldestTime = Infinity;
6464
+ for (const [name, time] of _shardLastAccess) {
6465
+ if (time < oldestTime) {
6466
+ oldestTime = time;
6467
+ oldest = name;
6468
+ }
6469
+ }
6470
+ if (oldest) {
6471
+ const client = _shards.get(oldest);
6472
+ if (client) {
6473
+ client.close();
6474
+ }
6475
+ _shards.delete(oldest);
6476
+ _shardLastAccess.delete(oldest);
6477
+ }
6478
+ }
6479
+ function evictIdleShards() {
6480
+ const now = Date.now();
6481
+ const toEvict = [];
6482
+ for (const [name, lastAccess] of _shardLastAccess) {
6483
+ if (now - lastAccess > SHARD_IDLE_MS) {
6484
+ toEvict.push(name);
6485
+ }
6486
+ }
6487
+ for (const name of toEvict) {
6488
+ const client = _shards.get(name);
6489
+ if (client) {
6490
+ client.close();
6491
+ }
6492
+ _shards.delete(name);
6493
+ _shardLastAccess.delete(name);
6494
+ }
6495
+ }
6496
+ function getOpenShardCount() {
6497
+ return _shards.size;
6498
+ }
6291
6499
  function disposeShards() {
6500
+ if (_evictionTimer) {
6501
+ clearInterval(_evictionTimer);
6502
+ _evictionTimer = null;
6503
+ }
6292
6504
  for (const [, client] of _shards) {
6293
6505
  client.close();
6294
6506
  }
6295
6507
  _shards.clear();
6508
+ _shardLastAccess.clear();
6296
6509
  _shardingEnabled = false;
6297
6510
  _encryptionKey = null;
6298
6511
  }
6299
- var SHARDS_DIR, _shards, _encryptionKey, _shardingEnabled;
6512
+ var SHARDS_DIR, SHARD_IDLE_MS, MAX_OPEN_SHARDS, EVICTION_INTERVAL_MS, _shards, _shardLastAccess, _evictionTimer, _encryptionKey, _shardingEnabled;
6300
6513
  var init_shard_manager = __esm({
6301
6514
  "src/lib/shard-manager.ts"() {
6302
6515
  "use strict";
6303
6516
  init_config();
6304
- SHARDS_DIR = path19.join(EXE_AI_DIR, "shards");
6517
+ SHARDS_DIR = path20.join(EXE_AI_DIR, "shards");
6518
+ SHARD_IDLE_MS = 5 * 60 * 1e3;
6519
+ MAX_OPEN_SHARDS = 10;
6520
+ EVICTION_INTERVAL_MS = 60 * 1e3;
6305
6521
  _shards = /* @__PURE__ */ new Map();
6522
+ _shardLastAccess = /* @__PURE__ */ new Map();
6523
+ _evictionTimer = null;
6306
6524
  _encryptionKey = null;
6307
6525
  _shardingEnabled = false;
6308
6526
  }
@@ -7287,7 +7505,7 @@ var init_git_task_sweep = __esm({
7287
7505
  init_config();
7288
7506
  init_session_key();
7289
7507
  init_employees();
7290
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync, unlinkSync as unlinkSync2, readdirSync } from "fs";
7508
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, readdirSync } from "fs";
7291
7509
  import { execSync as execSync3 } from "child_process";
7292
7510
  import path3 from "path";
7293
7511
  var CACHE_DIR = path3.join(EXE_AI_DIR, "session-cache");
@@ -7418,7 +7636,7 @@ process.stdin.on("end", async () => {
7418
7636
  const { getClient: getClient2 } = await Promise.resolve().then(() => (init_database(), database_exports));
7419
7637
  const { randomUUID: randomUUID4 } = await import("crypto");
7420
7638
  const client = getClient2();
7421
- const seScope = sessionScopeFilter();
7639
+ const seScope = strictSessionScopeFilter();
7422
7640
  const orphanResult = await client.execute({
7423
7641
  sql: `SELECT title, status FROM tasks WHERE assigned_to = ? AND status IN ('open', 'in_progress')${seScope.sql}`,
7424
7642
  args: [agent.agentId, ...seScope.args]
@@ -7462,8 +7680,10 @@ Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title
7462
7680
  let context;
7463
7681
  try {
7464
7682
  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]
7683
+ sql: `SELECT id, context FROM tasks
7684
+ WHERE title = ? AND assigned_to = ? AND status = 'in_progress'${seScope.sql}
7685
+ LIMIT 1`,
7686
+ args: [title, agent.agentId, ...seScope.args]
7467
7687
  });
7468
7688
  if (ctxResult.rows.length > 0) {
7469
7689
  context = ctxResult.rows[0].context ? String(ctxResult.rows[0].context) : void 0;
@@ -7474,12 +7694,14 @@ Orphaned tasks at session end: ${orphanResult.rows.map((r) => `"${String(r.title
7474
7694
  const match = findBestMatch2(taskForMatch, commits);
7475
7695
  if (match) {
7476
7696
  await client.execute({
7477
- sql: "UPDATE tasks SET status = 'done', result = ?, updated_at = ? WHERE title = ? AND assigned_to = ? AND status = 'in_progress'",
7697
+ sql: `UPDATE tasks SET status = 'done', result = ?, updated_at = ?
7698
+ WHERE title = ? AND assigned_to = ? AND status = 'in_progress'${seScope.sql}`,
7478
7699
  args: [
7479
7700
  `Auto-closed: session ended but matching commit ${match.commit.hash} found (score: ${match.score.toFixed(2)}). Message: "${match.commit.message}"`,
7480
7701
  (/* @__PURE__ */ new Date()).toISOString(),
7481
7702
  title,
7482
- agent.agentId
7703
+ agent.agentId,
7704
+ ...seScope.args
7483
7705
  ]
7484
7706
  });
7485
7707
  autoClosed.push(`"${title}" \u2192 commit ${match.commit.hash}`);