@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
@@ -307,9 +307,47 @@ var init_provider_table = __esm({
307
307
  }
308
308
  });
309
309
 
310
+ // src/lib/secure-files.ts
311
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
312
+ import { chmod, mkdir } from "fs/promises";
313
+ async function ensurePrivateDir(dirPath) {
314
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
315
+ try {
316
+ await chmod(dirPath, PRIVATE_DIR_MODE);
317
+ } catch {
318
+ }
319
+ }
320
+ function ensurePrivateDirSync(dirPath) {
321
+ mkdirSync2(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
322
+ try {
323
+ chmodSync(dirPath, PRIVATE_DIR_MODE);
324
+ } catch {
325
+ }
326
+ }
327
+ async function enforcePrivateFile(filePath) {
328
+ try {
329
+ await chmod(filePath, PRIVATE_FILE_MODE);
330
+ } catch {
331
+ }
332
+ }
333
+ function enforcePrivateFileSync(filePath) {
334
+ try {
335
+ if (existsSync2(filePath)) chmodSync(filePath, PRIVATE_FILE_MODE);
336
+ } catch {
337
+ }
338
+ }
339
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
340
+ var init_secure_files = __esm({
341
+ "src/lib/secure-files.ts"() {
342
+ "use strict";
343
+ PRIVATE_DIR_MODE = 448;
344
+ PRIVATE_FILE_MODE = 384;
345
+ }
346
+ });
347
+
310
348
  // src/lib/config.ts
311
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
312
- import { readFileSync as readFileSync2, existsSync as existsSync2, renameSync } from "fs";
349
+ import { readFile, writeFile } from "fs/promises";
350
+ import { readFileSync as readFileSync2, existsSync as existsSync3, renameSync } from "fs";
313
351
  import path2 from "path";
314
352
  import os2 from "os";
315
353
  function resolveDataDir() {
@@ -317,7 +355,7 @@ function resolveDataDir() {
317
355
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
318
356
  const newDir = path2.join(os2.homedir(), ".exe-os");
319
357
  const legacyDir = path2.join(os2.homedir(), ".exe-mem");
320
- if (!existsSync2(newDir) && existsSync2(legacyDir)) {
358
+ if (!existsSync3(newDir) && existsSync3(legacyDir)) {
321
359
  try {
322
360
  renameSync(legacyDir, newDir);
323
361
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -380,9 +418,9 @@ function normalizeAutoUpdate(raw) {
380
418
  }
381
419
  async function loadConfig() {
382
420
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
383
- await mkdir(dir, { recursive: true });
421
+ await ensurePrivateDir(dir);
384
422
  const configPath = path2.join(dir, "config.json");
385
- if (!existsSync2(configPath)) {
423
+ if (!existsSync3(configPath)) {
386
424
  return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
387
425
  }
388
426
  const raw = await readFile(configPath, "utf-8");
@@ -395,6 +433,7 @@ async function loadConfig() {
395
433
  `);
396
434
  try {
397
435
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
436
+ await enforcePrivateFile(configPath);
398
437
  } catch {
399
438
  }
400
439
  }
@@ -414,6 +453,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
414
453
  var init_config = __esm({
415
454
  "src/lib/config.ts"() {
416
455
  "use strict";
456
+ init_secure_files();
417
457
  EXE_AI_DIR = resolveDataDir();
418
458
  DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
419
459
  MODELS_DIR = path2.join(EXE_AI_DIR, "models");
@@ -518,10 +558,10 @@ var init_runtime_table = __esm({
518
558
  });
519
559
 
520
560
  // src/lib/agent-config.ts
521
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
561
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
522
562
  import path3 from "path";
523
563
  function loadAgentConfig() {
524
- if (!existsSync3(AGENT_CONFIG_PATH)) return {};
564
+ if (!existsSync4(AGENT_CONFIG_PATH)) return {};
525
565
  try {
526
566
  return JSON.parse(readFileSync3(AGENT_CONFIG_PATH, "utf-8"));
527
567
  } catch {
@@ -542,6 +582,7 @@ var init_agent_config = __esm({
542
582
  "use strict";
543
583
  init_config();
544
584
  init_runtime_table();
585
+ init_secure_files();
545
586
  AGENT_CONFIG_PATH = path3.join(EXE_AI_DIR, "agent-config.json");
546
587
  DEFAULT_MODELS = {
547
588
  claude: "claude-opus-4",
@@ -560,16 +601,16 @@ __export(intercom_queue_exports, {
560
601
  queueIntercom: () => queueIntercom,
561
602
  readQueue: () => readQueue
562
603
  });
563
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
604
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
564
605
  import path4 from "path";
565
606
  import os3 from "os";
566
607
  function ensureDir() {
567
608
  const dir = path4.dirname(QUEUE_PATH);
568
- if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
609
+ if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
569
610
  }
570
611
  function readQueue() {
571
612
  try {
572
- if (!existsSync4(QUEUE_PATH)) return [];
613
+ if (!existsSync5(QUEUE_PATH)) return [];
573
614
  return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
574
615
  } catch {
575
616
  return [];
@@ -734,7 +775,7 @@ var init_db_retry = __esm({
734
775
 
735
776
  // src/lib/employees.ts
736
777
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
737
- import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
778
+ import { existsSync as existsSync6, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
738
779
  import { execSync as execSync3 } from "child_process";
739
780
  import path5 from "path";
740
781
  import os4 from "os";
@@ -755,7 +796,7 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
755
796
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
756
797
  }
757
798
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
758
- if (!existsSync5(employeesPath)) return [];
799
+ if (!existsSync6(employeesPath)) return [];
759
800
  try {
760
801
  return JSON.parse(readFileSync5(employeesPath, "utf-8"));
761
802
  } catch {
@@ -1376,13 +1417,50 @@ var init_database_adapter = __esm({
1376
1417
  }
1377
1418
  });
1378
1419
 
1420
+ // src/lib/daemon-auth.ts
1421
+ import crypto from "crypto";
1422
+ import path7 from "path";
1423
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
1424
+ function normalizeToken(token) {
1425
+ if (!token) return null;
1426
+ const trimmed = token.trim();
1427
+ return trimmed.length > 0 ? trimmed : null;
1428
+ }
1429
+ function readDaemonToken() {
1430
+ try {
1431
+ if (!existsSync7(DAEMON_TOKEN_PATH)) return null;
1432
+ return normalizeToken(readFileSync6(DAEMON_TOKEN_PATH, "utf8"));
1433
+ } catch {
1434
+ return null;
1435
+ }
1436
+ }
1437
+ function ensureDaemonToken(seed) {
1438
+ const existing = readDaemonToken();
1439
+ if (existing) return existing;
1440
+ const token = normalizeToken(seed) ?? crypto.randomBytes(32).toString("hex");
1441
+ ensurePrivateDirSync(EXE_AI_DIR);
1442
+ writeFileSync5(DAEMON_TOKEN_PATH, `${token}
1443
+ `, "utf8");
1444
+ enforcePrivateFileSync(DAEMON_TOKEN_PATH);
1445
+ return token;
1446
+ }
1447
+ var DAEMON_TOKEN_PATH;
1448
+ var init_daemon_auth = __esm({
1449
+ "src/lib/daemon-auth.ts"() {
1450
+ "use strict";
1451
+ init_config();
1452
+ init_secure_files();
1453
+ DAEMON_TOKEN_PATH = path7.join(EXE_AI_DIR, "exed.token");
1454
+ }
1455
+ });
1456
+
1379
1457
  // src/lib/exe-daemon-client.ts
1380
1458
  import net from "net";
1381
1459
  import os6 from "os";
1382
1460
  import { spawn } from "child_process";
1383
1461
  import { randomUUID } from "crypto";
1384
- import { existsSync as existsSync6, unlinkSync as unlinkSync2, readFileSync as readFileSync6, openSync, closeSync, statSync } from "fs";
1385
- import path7 from "path";
1462
+ import { existsSync as existsSync8, unlinkSync as unlinkSync2, readFileSync as readFileSync7, openSync, closeSync, statSync } from "fs";
1463
+ import path8 from "path";
1386
1464
  import { fileURLToPath } from "url";
1387
1465
  function handleData(chunk) {
1388
1466
  _buffer += chunk.toString();
@@ -1410,9 +1488,9 @@ function handleData(chunk) {
1410
1488
  }
1411
1489
  }
1412
1490
  function cleanupStaleFiles() {
1413
- if (existsSync6(PID_PATH)) {
1491
+ if (existsSync8(PID_PATH)) {
1414
1492
  try {
1415
- const pid = parseInt(readFileSync6(PID_PATH, "utf8").trim(), 10);
1493
+ const pid = parseInt(readFileSync7(PID_PATH, "utf8").trim(), 10);
1416
1494
  if (pid > 0) {
1417
1495
  try {
1418
1496
  process.kill(pid, 0);
@@ -1433,11 +1511,11 @@ function cleanupStaleFiles() {
1433
1511
  }
1434
1512
  }
1435
1513
  function findPackageRoot() {
1436
- let dir = path7.dirname(fileURLToPath(import.meta.url));
1437
- const { root } = path7.parse(dir);
1514
+ let dir = path8.dirname(fileURLToPath(import.meta.url));
1515
+ const { root } = path8.parse(dir);
1438
1516
  while (dir !== root) {
1439
- if (existsSync6(path7.join(dir, "package.json"))) return dir;
1440
- dir = path7.dirname(dir);
1517
+ if (existsSync8(path8.join(dir, "package.json"))) return dir;
1518
+ dir = path8.dirname(dir);
1441
1519
  }
1442
1520
  return null;
1443
1521
  }
@@ -1463,16 +1541,17 @@ function spawnDaemon() {
1463
1541
  process.stderr.write("[exed-client] WARN: cannot find package root\n");
1464
1542
  return;
1465
1543
  }
1466
- const daemonPath = path7.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1467
- if (!existsSync6(daemonPath)) {
1544
+ const daemonPath = path8.join(pkgRoot, "dist", "lib", "exe-daemon.js");
1545
+ if (!existsSync8(daemonPath)) {
1468
1546
  process.stderr.write(`[exed-client] WARN: daemon script not found at ${daemonPath}
1469
1547
  `);
1470
1548
  return;
1471
1549
  }
1472
1550
  const resolvedPath = daemonPath;
1551
+ const daemonToken = ensureDaemonToken(process.env[DAEMON_TOKEN_ENV] ?? null);
1473
1552
  process.stderr.write(`[exed-client] Spawning daemon: ${resolvedPath}
1474
1553
  `);
1475
- const logPath = path7.join(path7.dirname(SOCKET_PATH), "exed.log");
1554
+ const logPath = path8.join(path8.dirname(SOCKET_PATH), "exed.log");
1476
1555
  let stderrFd = "ignore";
1477
1556
  try {
1478
1557
  stderrFd = openSync(logPath, "a");
@@ -1490,7 +1569,8 @@ function spawnDaemon() {
1490
1569
  TMUX_PANE: void 0,
1491
1570
  // Prevents resolveExeSession() from scoping to one session
1492
1571
  EXE_DAEMON_SOCK: SOCKET_PATH,
1493
- EXE_DAEMON_PID: PID_PATH
1572
+ EXE_DAEMON_PID: PID_PATH,
1573
+ [DAEMON_TOKEN_ENV]: daemonToken
1494
1574
  }
1495
1575
  });
1496
1576
  child.unref();
@@ -1597,13 +1677,14 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1597
1677
  return;
1598
1678
  }
1599
1679
  const id = randomUUID();
1680
+ const token = process.env[DAEMON_TOKEN_ENV] ?? readDaemonToken();
1600
1681
  const timer = setTimeout(() => {
1601
1682
  _pending.delete(id);
1602
1683
  resolve({ error: "Request timeout" });
1603
1684
  }, timeoutMs);
1604
1685
  _pending.set(id, { resolve, timer });
1605
1686
  try {
1606
- _socket.write(JSON.stringify({ id, ...payload }) + "\n");
1687
+ _socket.write(JSON.stringify({ id, token, ...payload }) + "\n");
1607
1688
  } catch {
1608
1689
  clearTimeout(timer);
1609
1690
  _pending.delete(id);
@@ -1614,17 +1695,19 @@ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1614
1695
  function isClientConnected() {
1615
1696
  return _connected;
1616
1697
  }
1617
- var SOCKET_PATH, PID_PATH, SPAWN_LOCK_PATH, SPAWN_LOCK_STALE_MS, CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, _socket, _connected, _buffer, _pending, MAX_BUFFER;
1698
+ 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;
1618
1699
  var init_exe_daemon_client = __esm({
1619
1700
  "src/lib/exe-daemon-client.ts"() {
1620
1701
  "use strict";
1621
1702
  init_config();
1622
- SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path7.join(EXE_AI_DIR, "exed.sock");
1623
- PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path7.join(EXE_AI_DIR, "exed.pid");
1624
- SPAWN_LOCK_PATH = path7.join(EXE_AI_DIR, "exed-spawn.lock");
1703
+ init_daemon_auth();
1704
+ SOCKET_PATH = process.env.EXE_DAEMON_SOCK ?? process.env.EXE_EMBED_SOCK ?? path8.join(EXE_AI_DIR, "exed.sock");
1705
+ PID_PATH = process.env.EXE_DAEMON_PID ?? process.env.EXE_EMBED_PID ?? path8.join(EXE_AI_DIR, "exed.pid");
1706
+ SPAWN_LOCK_PATH = path8.join(EXE_AI_DIR, "exed-spawn.lock");
1625
1707
  SPAWN_LOCK_STALE_MS = 3e4;
1626
1708
  CONNECT_TIMEOUT_MS = 15e3;
1627
1709
  REQUEST_TIMEOUT_MS = 3e4;
1710
+ DAEMON_TOKEN_ENV = "EXE_DAEMON_TOKEN";
1628
1711
  _socket = null;
1629
1712
  _connected = false;
1630
1713
  _buffer = "";
@@ -2203,6 +2286,7 @@ async function ensureSchema() {
2203
2286
  project TEXT NOT NULL,
2204
2287
  summary TEXT NOT NULL,
2205
2288
  task_file TEXT,
2289
+ session_scope TEXT,
2206
2290
  read INTEGER NOT NULL DEFAULT 0,
2207
2291
  created_at TEXT NOT NULL
2208
2292
  );
@@ -2211,7 +2295,7 @@ async function ensureSchema() {
2211
2295
  ON notifications(read);
2212
2296
 
2213
2297
  CREATE INDEX IF NOT EXISTS idx_notifications_agent
2214
- ON notifications(agent_id);
2298
+ ON notifications(agent_id, session_scope);
2215
2299
 
2216
2300
  CREATE INDEX IF NOT EXISTS idx_notifications_task_file
2217
2301
  ON notifications(task_file);
@@ -2249,6 +2333,7 @@ async function ensureSchema() {
2249
2333
  target_agent TEXT NOT NULL,
2250
2334
  target_project TEXT,
2251
2335
  target_device TEXT NOT NULL DEFAULT 'local',
2336
+ session_scope TEXT,
2252
2337
  content TEXT NOT NULL,
2253
2338
  priority TEXT DEFAULT 'normal',
2254
2339
  status TEXT DEFAULT 'pending',
@@ -2262,10 +2347,31 @@ async function ensureSchema() {
2262
2347
  );
2263
2348
 
2264
2349
  CREATE INDEX IF NOT EXISTS idx_messages_target
2265
- ON messages(target_agent, status);
2350
+ ON messages(target_agent, session_scope, status);
2266
2351
 
2267
2352
  CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
2268
- ON messages(target_agent, from_agent, server_seq);
2353
+ ON messages(target_agent, session_scope, from_agent, server_seq);
2354
+ `);
2355
+ try {
2356
+ await client.execute({
2357
+ sql: `ALTER TABLE notifications ADD COLUMN session_scope TEXT`,
2358
+ args: []
2359
+ });
2360
+ } catch {
2361
+ }
2362
+ try {
2363
+ await client.execute({
2364
+ sql: `ALTER TABLE messages ADD COLUMN session_scope TEXT`,
2365
+ args: []
2366
+ });
2367
+ } catch {
2368
+ }
2369
+ await client.executeMultiple(`
2370
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent_scope_read
2371
+ ON notifications(agent_id, session_scope, read, created_at);
2372
+
2373
+ CREATE INDEX IF NOT EXISTS idx_messages_target_scope_status
2374
+ ON messages(target_agent, session_scope, status, created_at);
2269
2375
  `);
2270
2376
  try {
2271
2377
  await client.execute({
@@ -2849,6 +2955,13 @@ async function ensureSchema() {
2849
2955
  } catch {
2850
2956
  }
2851
2957
  }
2958
+ try {
2959
+ await client.execute({
2960
+ sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
2961
+ args: []
2962
+ });
2963
+ } catch {
2964
+ }
2852
2965
  }
2853
2966
  async function disposeDatabase() {
2854
2967
  if (_walCheckpointTimer) {
@@ -2887,18 +3000,21 @@ var init_database = __esm({
2887
3000
  });
2888
3001
 
2889
3002
  // src/lib/license.ts
2890
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
3003
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
2891
3004
  import { randomUUID as randomUUID2 } from "crypto";
2892
- import path8 from "path";
3005
+ import { createRequire as createRequire2 } from "module";
3006
+ import { pathToFileURL as pathToFileURL2 } from "url";
3007
+ import os7 from "os";
3008
+ import path9 from "path";
2893
3009
  import { jwtVerify, importSPKI } from "jose";
2894
3010
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
2895
3011
  var init_license = __esm({
2896
3012
  "src/lib/license.ts"() {
2897
3013
  "use strict";
2898
3014
  init_config();
2899
- LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
2900
- CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
2901
- DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
3015
+ LICENSE_PATH = path9.join(EXE_AI_DIR, "license.key");
3016
+ CACHE_PATH = path9.join(EXE_AI_DIR, "license-cache.json");
3017
+ DEVICE_ID_PATH = path9.join(EXE_AI_DIR, "device-id");
2902
3018
  PLAN_LIMITS = {
2903
3019
  free: { devices: 1, employees: 1, memories: 5e3 },
2904
3020
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -2910,12 +3026,12 @@ var init_license = __esm({
2910
3026
  });
2911
3027
 
2912
3028
  // src/lib/plan-limits.ts
2913
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
2914
- import path9 from "path";
3029
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
3030
+ import path10 from "path";
2915
3031
  function getLicenseSync() {
2916
3032
  try {
2917
- if (!existsSync8(CACHE_PATH2)) return freeLicense();
2918
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
3033
+ if (!existsSync10(CACHE_PATH2)) return freeLicense();
3034
+ const raw = JSON.parse(readFileSync9(CACHE_PATH2, "utf8"));
2919
3035
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
2920
3036
  const parts = raw.token.split(".");
2921
3037
  if (parts.length !== 3) return freeLicense();
@@ -2953,8 +3069,8 @@ function assertEmployeeLimitSync(rosterPath) {
2953
3069
  const filePath = rosterPath ?? EMPLOYEES_PATH;
2954
3070
  let count = 0;
2955
3071
  try {
2956
- if (existsSync8(filePath)) {
2957
- const raw = readFileSync8(filePath, "utf8");
3072
+ if (existsSync10(filePath)) {
3073
+ const raw = readFileSync9(filePath, "utf8");
2958
3074
  const employees = JSON.parse(raw);
2959
3075
  count = Array.isArray(employees) ? employees.length : 0;
2960
3076
  }
@@ -2983,29 +3099,30 @@ var init_plan_limits = __esm({
2983
3099
  this.name = "PlanLimitError";
2984
3100
  }
2985
3101
  };
2986
- CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
3102
+ CACHE_PATH2 = path10.join(EXE_AI_DIR, "license-cache.json");
2987
3103
  }
2988
3104
  });
2989
3105
 
2990
3106
  // src/lib/notifications.ts
2991
- import crypto from "crypto";
2992
- import path10 from "path";
2993
- import os7 from "os";
3107
+ import crypto2 from "crypto";
3108
+ import path11 from "path";
3109
+ import os8 from "os";
2994
3110
  import {
2995
- readFileSync as readFileSync9,
3111
+ readFileSync as readFileSync10,
2996
3112
  readdirSync,
2997
3113
  unlinkSync as unlinkSync3,
2998
- existsSync as existsSync9,
3114
+ existsSync as existsSync11,
2999
3115
  rmdirSync
3000
3116
  } from "fs";
3001
3117
  async function writeNotification(notification) {
3002
3118
  try {
3003
3119
  const client = getClient();
3004
- const id = crypto.randomUUID();
3120
+ const id = crypto2.randomUUID();
3005
3121
  const now = (/* @__PURE__ */ new Date()).toISOString();
3122
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
3006
3123
  await client.execute({
3007
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
3008
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3124
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
3125
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
3009
3126
  args: [
3010
3127
  id,
3011
3128
  notification.agentId,
@@ -3014,6 +3131,7 @@ async function writeNotification(notification) {
3014
3131
  notification.project,
3015
3132
  notification.summary,
3016
3133
  notification.taskFile ?? null,
3134
+ sessionScope,
3017
3135
  now
3018
3136
  ]
3019
3137
  });
@@ -3022,12 +3140,14 @@ async function writeNotification(notification) {
3022
3140
  `);
3023
3141
  }
3024
3142
  }
3025
- async function markAsReadByTaskFile(taskFile) {
3143
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
3026
3144
  try {
3027
3145
  const client = getClient();
3146
+ const scope = strictSessionScopeFilter(sessionScope);
3028
3147
  await client.execute({
3029
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
3030
- args: [taskFile]
3148
+ sql: `UPDATE notifications SET read = 1
3149
+ WHERE task_file = ? AND read = 0${scope.sql}`,
3150
+ args: [taskFile, ...scope.args]
3031
3151
  });
3032
3152
  } catch {
3033
3153
  }
@@ -3036,11 +3156,12 @@ var init_notifications = __esm({
3036
3156
  "src/lib/notifications.ts"() {
3037
3157
  "use strict";
3038
3158
  init_database();
3159
+ init_task_scope();
3039
3160
  }
3040
3161
  });
3041
3162
 
3042
3163
  // src/lib/session-kill-telemetry.ts
3043
- import crypto2 from "crypto";
3164
+ import crypto3 from "crypto";
3044
3165
  async function recordSessionKill(input) {
3045
3166
  try {
3046
3167
  const client = getClient();
@@ -3050,7 +3171,7 @@ async function recordSessionKill(input) {
3050
3171
  ticks_idle, estimated_tokens_saved)
3051
3172
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
3052
3173
  args: [
3053
- crypto2.randomUUID(),
3174
+ crypto3.randomUUID(),
3054
3175
  input.sessionName,
3055
3176
  input.agentId,
3056
3177
  (/* @__PURE__ */ new Date()).toISOString(),
@@ -3128,6 +3249,110 @@ var init_state_bus = __esm({
3128
3249
  }
3129
3250
  });
3130
3251
 
3252
+ // src/lib/project-name.ts
3253
+ import { execSync as execSync4 } from "child_process";
3254
+ import path12 from "path";
3255
+ function getProjectName(cwd) {
3256
+ const dir = cwd ?? process.cwd();
3257
+ if (_cached2 && _cachedCwd === dir) return _cached2;
3258
+ try {
3259
+ let repoRoot;
3260
+ try {
3261
+ const gitCommonDir = execSync4("git rev-parse --path-format=absolute --git-common-dir", {
3262
+ cwd: dir,
3263
+ encoding: "utf8",
3264
+ timeout: 2e3,
3265
+ stdio: ["pipe", "pipe", "pipe"]
3266
+ }).trim();
3267
+ repoRoot = path12.dirname(gitCommonDir);
3268
+ } catch {
3269
+ repoRoot = execSync4("git rev-parse --show-toplevel", {
3270
+ cwd: dir,
3271
+ encoding: "utf8",
3272
+ timeout: 2e3,
3273
+ stdio: ["pipe", "pipe", "pipe"]
3274
+ }).trim();
3275
+ }
3276
+ _cached2 = path12.basename(repoRoot);
3277
+ _cachedCwd = dir;
3278
+ return _cached2;
3279
+ } catch {
3280
+ _cached2 = path12.basename(dir);
3281
+ _cachedCwd = dir;
3282
+ return _cached2;
3283
+ }
3284
+ }
3285
+ var _cached2, _cachedCwd;
3286
+ var init_project_name = __esm({
3287
+ "src/lib/project-name.ts"() {
3288
+ "use strict";
3289
+ _cached2 = null;
3290
+ _cachedCwd = null;
3291
+ }
3292
+ });
3293
+
3294
+ // src/lib/session-scope.ts
3295
+ var session_scope_exports = {};
3296
+ __export(session_scope_exports, {
3297
+ assertSessionScope: () => assertSessionScope,
3298
+ findSessionForProject: () => findSessionForProject,
3299
+ getSessionProject: () => getSessionProject
3300
+ });
3301
+ function getSessionProject(sessionName) {
3302
+ const sessions = listSessions();
3303
+ const entry = sessions.find((s) => s.windowName === sessionName);
3304
+ if (!entry) return null;
3305
+ const parts = entry.projectDir.split("/").filter(Boolean);
3306
+ return parts[parts.length - 1] ?? null;
3307
+ }
3308
+ function findSessionForProject(projectName) {
3309
+ const sessions = listSessions();
3310
+ for (const s of sessions) {
3311
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
3312
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
3313
+ }
3314
+ return null;
3315
+ }
3316
+ function assertSessionScope(actionType, targetProject) {
3317
+ try {
3318
+ const currentProject = getProjectName();
3319
+ const exeSession = resolveExeSession();
3320
+ if (!exeSession) {
3321
+ return { allowed: true, reason: "no_session" };
3322
+ }
3323
+ if (currentProject === targetProject) {
3324
+ return {
3325
+ allowed: true,
3326
+ reason: "same_session",
3327
+ currentProject,
3328
+ targetProject
3329
+ };
3330
+ }
3331
+ process.stderr.write(
3332
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
3333
+ `
3334
+ );
3335
+ return {
3336
+ allowed: false,
3337
+ reason: "cross_session_denied",
3338
+ currentProject,
3339
+ targetProject,
3340
+ targetSession: findSessionForProject(targetProject)?.windowName
3341
+ };
3342
+ } catch {
3343
+ return { allowed: true, reason: "no_session" };
3344
+ }
3345
+ }
3346
+ var init_session_scope = __esm({
3347
+ "src/lib/session-scope.ts"() {
3348
+ "use strict";
3349
+ init_session_registry();
3350
+ init_project_name();
3351
+ init_tmux_routing();
3352
+ init_employees();
3353
+ }
3354
+ });
3355
+
3131
3356
  // src/lib/tasks-crud.ts
3132
3357
  var tasks_crud_exports = {};
3133
3358
  __export(tasks_crud_exports, {
@@ -3145,12 +3370,12 @@ __export(tasks_crud_exports, {
3145
3370
  updateTaskStatus: () => updateTaskStatus,
3146
3371
  writeCheckpoint: () => writeCheckpoint
3147
3372
  });
3148
- import crypto3 from "crypto";
3149
- import path11 from "path";
3150
- import os8 from "os";
3151
- import { execSync as execSync4 } from "child_process";
3373
+ import crypto4 from "crypto";
3374
+ import path13 from "path";
3375
+ import os9 from "os";
3376
+ import { execSync as execSync5 } from "child_process";
3152
3377
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
3153
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
3378
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
3154
3379
  async function writeCheckpoint(input) {
3155
3380
  const client = getClient();
3156
3381
  const row = await resolveTask(client, input.taskId);
@@ -3266,13 +3491,28 @@ async function resolveTask(client, identifier, scopeSession) {
3266
3491
  }
3267
3492
  async function createTaskCore(input) {
3268
3493
  const client = getClient();
3269
- const id = crypto3.randomUUID();
3494
+ const id = crypto4.randomUUID();
3270
3495
  const now = (/* @__PURE__ */ new Date()).toISOString();
3271
3496
  const slug = slugify(input.title);
3272
3497
  let earlySessionScope = null;
3498
+ let scopeMismatchWarning;
3273
3499
  try {
3274
3500
  const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3275
- earlySessionScope = resolveExeSession2();
3501
+ const resolved = resolveExeSession2();
3502
+ if (resolved && input.projectName) {
3503
+ const { getSessionProject: getSessionProject2 } = await Promise.resolve().then(() => (init_session_scope(), session_scope_exports));
3504
+ const sessionProject = getSessionProject2(resolved);
3505
+ if (sessionProject && sessionProject !== input.projectName) {
3506
+ scopeMismatchWarning = `session/project mismatch: session "${resolved}" owns "${sessionProject}" but task targets "${input.projectName}". Routed to default scope.`;
3507
+ process.stderr.write(`[create_task] ${scopeMismatchWarning}
3508
+ `);
3509
+ earlySessionScope = null;
3510
+ } else {
3511
+ earlySessionScope = resolved;
3512
+ }
3513
+ } else {
3514
+ earlySessionScope = resolved;
3515
+ }
3276
3516
  } catch {
3277
3517
  }
3278
3518
  const scope = earlySessionScope ?? "default";
@@ -3323,10 +3563,14 @@ async function createTaskCore(input) {
3323
3563
  ${laneWarning}` : laneWarning;
3324
3564
  }
3325
3565
  }
3566
+ if (scopeMismatchWarning) {
3567
+ warning = warning ? `${warning}
3568
+ ${scopeMismatchWarning}` : scopeMismatchWarning;
3569
+ }
3326
3570
  if (input.baseDir) {
3327
3571
  try {
3328
- await mkdir3(path11.join(input.baseDir, "exe", "output"), { recursive: true });
3329
- await mkdir3(path11.join(input.baseDir, "exe", "research"), { recursive: true });
3572
+ await mkdir3(path13.join(input.baseDir, "exe", "output"), { recursive: true });
3573
+ await mkdir3(path13.join(input.baseDir, "exe", "research"), { recursive: true });
3330
3574
  await ensureArchitectureDoc(input.baseDir, input.projectName);
3331
3575
  await ensureGitignoreExe(input.baseDir);
3332
3576
  } catch {
@@ -3362,13 +3606,19 @@ ${laneWarning}` : laneWarning;
3362
3606
  });
3363
3607
  if (input.baseDir) {
3364
3608
  try {
3365
- const EXE_OS_DIR = path11.join(os8.homedir(), ".exe-os");
3366
- const mdPath = path11.join(EXE_OS_DIR, taskFile);
3367
- const mdDir = path11.dirname(mdPath);
3368
- if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
3609
+ const EXE_OS_DIR = path13.join(os9.homedir(), ".exe-os");
3610
+ const mdPath = path13.join(EXE_OS_DIR, taskFile);
3611
+ const mdDir = path13.dirname(mdPath);
3612
+ if (!existsSync12(mdDir)) await mkdir3(mdDir, { recursive: true });
3369
3613
  const reviewer = input.reviewer ?? input.assignedBy;
3370
3614
  const mdContent = `# ${input.title}
3371
3615
 
3616
+ ## MANDATORY: When done
3617
+
3618
+ You MUST call update_task with status "done" and a result summary when finished.
3619
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3620
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
3621
+
3372
3622
  **ID:** ${id}
3373
3623
  **Status:** ${initialStatus}
3374
3624
  **Priority:** ${input.priority}
@@ -3382,12 +3632,6 @@ ${laneWarning}` : laneWarning;
3382
3632
  ## Context
3383
3633
 
3384
3634
  ${input.context}
3385
-
3386
- ## MANDATORY: When done
3387
-
3388
- You MUST call update_task with status "done" and a result summary when finished.
3389
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3390
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
3391
3635
  `;
3392
3636
  await writeFile3(mdPath, mdContent, "utf-8");
3393
3637
  } catch (err) {
@@ -3469,14 +3713,14 @@ function isTmuxSessionAlive(identifier) {
3469
3713
  if (!identifier || identifier === "unknown") return true;
3470
3714
  try {
3471
3715
  if (identifier.startsWith("%")) {
3472
- const output = execSync4("tmux list-panes -a -F '#{pane_id}'", {
3716
+ const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
3473
3717
  timeout: 2e3,
3474
3718
  encoding: "utf8",
3475
3719
  stdio: ["pipe", "pipe", "pipe"]
3476
3720
  });
3477
3721
  return output.split("\n").some((l) => l.trim() === identifier);
3478
3722
  } else {
3479
- execSync4(`tmux has-session -t ${JSON.stringify(identifier)}`, {
3723
+ execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
3480
3724
  timeout: 2e3,
3481
3725
  stdio: ["pipe", "pipe", "pipe"]
3482
3726
  });
@@ -3485,7 +3729,7 @@ function isTmuxSessionAlive(identifier) {
3485
3729
  } catch {
3486
3730
  if (identifier.startsWith("%")) return true;
3487
3731
  try {
3488
- execSync4("tmux list-sessions", {
3732
+ execSync5("tmux list-sessions", {
3489
3733
  timeout: 2e3,
3490
3734
  stdio: ["pipe", "pipe", "pipe"]
3491
3735
  });
@@ -3500,12 +3744,12 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
3500
3744
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
3501
3745
  try {
3502
3746
  const since = new Date(taskCreatedAt).toISOString();
3503
- const branch = execSync4(
3747
+ const branch = execSync5(
3504
3748
  "git rev-parse --abbrev-ref HEAD 2>/dev/null",
3505
3749
  { encoding: "utf8", timeout: 3e3 }
3506
3750
  ).trim();
3507
3751
  const branchArg = branch && branch !== "HEAD" ? branch : "";
3508
- const commitCount = execSync4(
3752
+ const commitCount = execSync5(
3509
3753
  `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
3510
3754
  { encoding: "utf8", timeout: 5e3 }
3511
3755
  ).trim();
@@ -3636,7 +3880,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
3636
3880
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
3637
3881
  } catch {
3638
3882
  }
3639
- if (input.status === "done" || input.status === "cancelled") {
3883
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
3640
3884
  try {
3641
3885
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
3642
3886
  clearQueueForAgent2(String(row.assigned_to));
@@ -3665,9 +3909,9 @@ async function deleteTaskCore(taskId, _baseDir) {
3665
3909
  return { taskFile, assignedTo, assignedBy, taskSlug };
3666
3910
  }
3667
3911
  async function ensureArchitectureDoc(baseDir, projectName) {
3668
- const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
3912
+ const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
3669
3913
  try {
3670
- if (existsSync10(archPath)) return;
3914
+ if (existsSync12(archPath)) return;
3671
3915
  const template = [
3672
3916
  `# ${projectName} \u2014 System Architecture`,
3673
3917
  "",
@@ -3700,10 +3944,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
3700
3944
  }
3701
3945
  }
3702
3946
  async function ensureGitignoreExe(baseDir) {
3703
- const gitignorePath = path11.join(baseDir, ".gitignore");
3947
+ const gitignorePath = path13.join(baseDir, ".gitignore");
3704
3948
  try {
3705
- if (existsSync10(gitignorePath)) {
3706
- const content = readFileSync10(gitignorePath, "utf-8");
3949
+ if (existsSync12(gitignorePath)) {
3950
+ const content = readFileSync11(gitignorePath, "utf-8");
3707
3951
  if (/^\/?exe\/?$/m.test(content)) return;
3708
3952
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3709
3953
  } else {
@@ -3734,58 +3978,42 @@ var init_tasks_crud = __esm({
3734
3978
  });
3735
3979
 
3736
3980
  // src/lib/tasks-review.ts
3737
- import path12 from "path";
3738
- import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
3981
+ import path14 from "path";
3982
+ import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
3739
3983
  async function countPendingReviews(sessionScope) {
3740
3984
  const client = getClient();
3741
- if (sessionScope) {
3742
- const result2 = await client.execute({
3743
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND session_scope = ?",
3744
- args: [sessionScope]
3745
- });
3746
- return Number(result2.rows[0]?.cnt) || 0;
3747
- }
3985
+ const scope = strictSessionScopeFilter(
3986
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3987
+ );
3748
3988
  const result = await client.execute({
3749
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3750
- args: []
3989
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3990
+ WHERE status = 'needs_review'${scope.sql}`,
3991
+ args: [...scope.args]
3751
3992
  });
3752
3993
  return Number(result.rows[0]?.cnt) || 0;
3753
3994
  }
3754
3995
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3755
3996
  const client = getClient();
3756
- if (sessionScope) {
3757
- const result2 = await client.execute({
3758
- sql: `SELECT COUNT(*) as cnt FROM tasks
3759
- WHERE status = 'needs_review' AND updated_at > ?
3760
- AND session_scope = ?`,
3761
- args: [sinceIso, sessionScope]
3762
- });
3763
- return Number(result2.rows[0]?.cnt) || 0;
3764
- }
3997
+ const scope = strictSessionScopeFilter(
3998
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3999
+ );
3765
4000
  const result = await client.execute({
3766
4001
  sql: `SELECT COUNT(*) as cnt FROM tasks
3767
- WHERE status = 'needs_review' AND updated_at > ?`,
3768
- args: [sinceIso]
4002
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
4003
+ args: [sinceIso, ...scope.args]
3769
4004
  });
3770
4005
  return Number(result.rows[0]?.cnt) || 0;
3771
4006
  }
3772
4007
  async function listPendingReviews(limit, sessionScope) {
3773
4008
  const client = getClient();
3774
- if (sessionScope) {
3775
- const result2 = await client.execute({
3776
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3777
- WHERE status = 'needs_review'
3778
- AND session_scope = ?
3779
- ORDER BY updated_at ASC LIMIT ?`,
3780
- args: [sessionScope, limit]
3781
- });
3782
- return result2.rows;
3783
- }
4009
+ const scope = strictSessionScopeFilter(
4010
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
4011
+ );
3784
4012
  const result = await client.execute({
3785
4013
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3786
- WHERE status = 'needs_review'
4014
+ WHERE status = 'needs_review'${scope.sql}
3787
4015
  ORDER BY updated_at ASC LIMIT ?`,
3788
- args: [limit]
4016
+ args: [...scope.args, limit]
3789
4017
  });
3790
4018
  return result.rows;
3791
4019
  }
@@ -3797,7 +4025,7 @@ async function cleanupOrphanedReviews() {
3797
4025
  WHERE status IN ('open', 'needs_review', 'in_progress')
3798
4026
  AND assigned_by = 'system'
3799
4027
  AND title LIKE 'Review:%'
3800
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
4028
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
3801
4029
  args: [now]
3802
4030
  });
3803
4031
  const r1b = await client.execute({
@@ -3916,11 +4144,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3916
4144
  );
3917
4145
  }
3918
4146
  try {
3919
- const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3920
- if (existsSync11(cacheDir)) {
4147
+ const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
4148
+ if (existsSync13(cacheDir)) {
3921
4149
  for (const f of readdirSync2(cacheDir)) {
3922
4150
  if (f.startsWith("review-notified-")) {
3923
- unlinkSync4(path12.join(cacheDir, f));
4151
+ unlinkSync4(path14.join(cacheDir, f));
3924
4152
  }
3925
4153
  }
3926
4154
  }
@@ -3937,11 +4165,12 @@ var init_tasks_review = __esm({
3937
4165
  init_tmux_routing();
3938
4166
  init_session_key();
3939
4167
  init_state_bus();
4168
+ init_task_scope();
3940
4169
  }
3941
4170
  });
3942
4171
 
3943
4172
  // src/lib/tasks-chain.ts
3944
- import path13 from "path";
4173
+ import path15 from "path";
3945
4174
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
3946
4175
  async function cascadeUnblock(taskId, baseDir, now) {
3947
4176
  const client = getClient();
@@ -3958,7 +4187,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
3958
4187
  });
3959
4188
  for (const ur of unblockedRows.rows) {
3960
4189
  try {
3961
- const ubFile = path13.join(baseDir, String(ur.task_file));
4190
+ const ubFile = path15.join(baseDir, String(ur.task_file));
3962
4191
  let ubContent = await readFile3(ubFile, "utf-8");
3963
4192
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
3964
4193
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -3993,7 +4222,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
3993
4222
  const scScope = sessionScopeFilter();
3994
4223
  const remaining = await client.execute({
3995
4224
  sql: `SELECT COUNT(*) as cnt FROM tasks
3996
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
4225
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
3997
4226
  args: [parentTaskId, ...scScope.args]
3998
4227
  });
3999
4228
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -4025,110 +4254,6 @@ var init_tasks_chain = __esm({
4025
4254
  }
4026
4255
  });
4027
4256
 
4028
- // src/lib/project-name.ts
4029
- import { execSync as execSync5 } from "child_process";
4030
- import path14 from "path";
4031
- function getProjectName(cwd) {
4032
- const dir = cwd ?? process.cwd();
4033
- if (_cached2 && _cachedCwd === dir) return _cached2;
4034
- try {
4035
- let repoRoot;
4036
- try {
4037
- const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
4038
- cwd: dir,
4039
- encoding: "utf8",
4040
- timeout: 2e3,
4041
- stdio: ["pipe", "pipe", "pipe"]
4042
- }).trim();
4043
- repoRoot = path14.dirname(gitCommonDir);
4044
- } catch {
4045
- repoRoot = execSync5("git rev-parse --show-toplevel", {
4046
- cwd: dir,
4047
- encoding: "utf8",
4048
- timeout: 2e3,
4049
- stdio: ["pipe", "pipe", "pipe"]
4050
- }).trim();
4051
- }
4052
- _cached2 = path14.basename(repoRoot);
4053
- _cachedCwd = dir;
4054
- return _cached2;
4055
- } catch {
4056
- _cached2 = path14.basename(dir);
4057
- _cachedCwd = dir;
4058
- return _cached2;
4059
- }
4060
- }
4061
- var _cached2, _cachedCwd;
4062
- var init_project_name = __esm({
4063
- "src/lib/project-name.ts"() {
4064
- "use strict";
4065
- _cached2 = null;
4066
- _cachedCwd = null;
4067
- }
4068
- });
4069
-
4070
- // src/lib/session-scope.ts
4071
- var session_scope_exports = {};
4072
- __export(session_scope_exports, {
4073
- assertSessionScope: () => assertSessionScope,
4074
- findSessionForProject: () => findSessionForProject,
4075
- getSessionProject: () => getSessionProject
4076
- });
4077
- function getSessionProject(sessionName) {
4078
- const sessions = listSessions();
4079
- const entry = sessions.find((s) => s.windowName === sessionName);
4080
- if (!entry) return null;
4081
- const parts = entry.projectDir.split("/").filter(Boolean);
4082
- return parts[parts.length - 1] ?? null;
4083
- }
4084
- function findSessionForProject(projectName) {
4085
- const sessions = listSessions();
4086
- for (const s of sessions) {
4087
- const proj = s.projectDir.split("/").filter(Boolean).pop();
4088
- if (proj === projectName && isCoordinatorName(s.agentId)) return s;
4089
- }
4090
- return null;
4091
- }
4092
- function assertSessionScope(actionType, targetProject) {
4093
- try {
4094
- const currentProject = getProjectName();
4095
- const exeSession = resolveExeSession();
4096
- if (!exeSession) {
4097
- return { allowed: true, reason: "no_session" };
4098
- }
4099
- if (currentProject === targetProject) {
4100
- return {
4101
- allowed: true,
4102
- reason: "same_session",
4103
- currentProject,
4104
- targetProject
4105
- };
4106
- }
4107
- process.stderr.write(
4108
- `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
4109
- `
4110
- );
4111
- return {
4112
- allowed: false,
4113
- reason: "cross_session_denied",
4114
- currentProject,
4115
- targetProject,
4116
- targetSession: findSessionForProject(targetProject)?.windowName
4117
- };
4118
- } catch {
4119
- return { allowed: true, reason: "no_session" };
4120
- }
4121
- }
4122
- var init_session_scope = __esm({
4123
- "src/lib/session-scope.ts"() {
4124
- "use strict";
4125
- init_session_registry();
4126
- init_project_name();
4127
- init_tmux_routing();
4128
- init_employees();
4129
- }
4130
- });
4131
-
4132
4257
  // src/lib/tasks-notify.ts
4133
4258
  async function dispatchTaskToEmployee(input) {
4134
4259
  if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
@@ -4196,10 +4321,10 @@ var init_tasks_notify = __esm({
4196
4321
  });
4197
4322
 
4198
4323
  // src/lib/behaviors.ts
4199
- import crypto4 from "crypto";
4324
+ import crypto5 from "crypto";
4200
4325
  async function storeBehavior(opts) {
4201
4326
  const client = getClient();
4202
- const id = crypto4.randomUUID();
4327
+ const id = crypto5.randomUUID();
4203
4328
  const now = (/* @__PURE__ */ new Date()).toISOString();
4204
4329
  await client.execute({
4205
4330
  sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
@@ -4228,7 +4353,7 @@ __export(skill_learning_exports, {
4228
4353
  storeTrajectory: () => storeTrajectory,
4229
4354
  sweepTrajectories: () => sweepTrajectories
4230
4355
  });
4231
- import crypto5 from "crypto";
4356
+ import crypto6 from "crypto";
4232
4357
  async function extractTrajectory(taskId, agentId) {
4233
4358
  const client = getClient();
4234
4359
  const result = await client.execute({
@@ -4257,11 +4382,11 @@ async function extractTrajectory(taskId, agentId) {
4257
4382
  return signature;
4258
4383
  }
4259
4384
  function hashSignature(signature) {
4260
- return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4385
+ return crypto6.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
4261
4386
  }
4262
4387
  async function storeTrajectory(opts) {
4263
4388
  const client = getClient();
4264
- const id = crypto5.randomUUID();
4389
+ const id = crypto6.randomUUID();
4265
4390
  const now = (/* @__PURE__ */ new Date()).toISOString();
4266
4391
  const signatureHash = hashSignature(opts.signature);
4267
4392
  await client.execute({
@@ -4526,8 +4651,8 @@ __export(tasks_exports, {
4526
4651
  updateTaskStatus: () => updateTaskStatus,
4527
4652
  writeCheckpoint: () => writeCheckpoint
4528
4653
  });
4529
- import path15 from "path";
4530
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
4654
+ import path16 from "path";
4655
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
4531
4656
  async function createTask(input) {
4532
4657
  const result = await createTaskCore(input);
4533
4658
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -4546,12 +4671,12 @@ async function updateTask(input) {
4546
4671
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
4547
4672
  try {
4548
4673
  const agent = String(row.assigned_to);
4549
- const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
4550
- const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
4674
+ const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
4675
+ const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
4551
4676
  if (input.status === "in_progress") {
4552
4677
  mkdirSync5(cacheDir, { recursive: true });
4553
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4554
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
4678
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4679
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
4555
4680
  try {
4556
4681
  unlinkSync5(cachePath);
4557
4682
  } catch {
@@ -4559,10 +4684,10 @@ async function updateTask(input) {
4559
4684
  }
4560
4685
  } catch {
4561
4686
  }
4562
- if (input.status === "done") {
4687
+ if (input.status === "done" || input.status === "closed") {
4563
4688
  await cleanupReviewFile(row, taskFile, input.baseDir);
4564
4689
  }
4565
- if (input.status === "done" || input.status === "cancelled") {
4690
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
4566
4691
  try {
4567
4692
  const client = getClient();
4568
4693
  const taskTitle = String(row.title);
@@ -4578,7 +4703,7 @@ async function updateTask(input) {
4578
4703
  if (!isCoordinatorName(assignedAgent)) {
4579
4704
  try {
4580
4705
  const draftClient = getClient();
4581
- if (input.status === "done") {
4706
+ if (input.status === "done" || input.status === "closed") {
4582
4707
  await draftClient.execute({
4583
4708
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
4584
4709
  args: [assignedAgent]
@@ -4595,7 +4720,7 @@ async function updateTask(input) {
4595
4720
  try {
4596
4721
  const client = getClient();
4597
4722
  const cascaded = await client.execute({
4598
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
4723
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
4599
4724
  WHERE parent_task_id = ? AND status = 'needs_review'`,
4600
4725
  args: [now, taskId]
4601
4726
  });
@@ -4608,14 +4733,14 @@ async function updateTask(input) {
4608
4733
  } catch {
4609
4734
  }
4610
4735
  }
4611
- const isTerminal = input.status === "done" || input.status === "needs_review";
4736
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
4612
4737
  if (isTerminal) {
4613
4738
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
4614
4739
  if (!isCoordinator) {
4615
4740
  notifyTaskDone();
4616
4741
  }
4617
4742
  await markTaskNotificationsRead(taskFile);
4618
- if (input.status === "done") {
4743
+ if (input.status === "done" || input.status === "closed") {
4619
4744
  try {
4620
4745
  await cascadeUnblock(taskId, input.baseDir, now);
4621
4746
  } catch {
@@ -4635,7 +4760,7 @@ async function updateTask(input) {
4635
4760
  }
4636
4761
  }
4637
4762
  }
4638
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4763
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4639
4764
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4640
4765
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4641
4766
  taskId,
@@ -5007,6 +5132,7 @@ __export(tmux_routing_exports, {
5007
5132
  isEmployeeAlive: () => isEmployeeAlive,
5008
5133
  isExeSession: () => isExeSession,
5009
5134
  isSessionBusy: () => isSessionBusy,
5135
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
5010
5136
  notifyParentExe: () => notifyParentExe,
5011
5137
  parseParentExe: () => parseParentExe,
5012
5138
  registerParentExe: () => registerParentExe,
@@ -5017,13 +5143,13 @@ __export(tmux_routing_exports, {
5017
5143
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
5018
5144
  });
5019
5145
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
5020
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync, readdirSync as readdirSync3 } from "fs";
5021
- import path16 from "path";
5022
- import os9 from "os";
5146
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, existsSync as existsSync14, appendFileSync, readdirSync as readdirSync3 } from "fs";
5147
+ import path17 from "path";
5148
+ import os10 from "os";
5023
5149
  import { fileURLToPath as fileURLToPath2 } from "url";
5024
5150
  import { unlinkSync as unlinkSync6 } from "fs";
5025
5151
  function spawnLockPath(sessionName) {
5026
- return path16.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5152
+ return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
5027
5153
  }
5028
5154
  function isProcessAlive(pid) {
5029
5155
  try {
@@ -5034,13 +5160,13 @@ function isProcessAlive(pid) {
5034
5160
  }
5035
5161
  }
5036
5162
  function acquireSpawnLock2(sessionName) {
5037
- if (!existsSync12(SPAWN_LOCK_DIR)) {
5163
+ if (!existsSync14(SPAWN_LOCK_DIR)) {
5038
5164
  mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
5039
5165
  }
5040
5166
  const lockFile = spawnLockPath(sessionName);
5041
- if (existsSync12(lockFile)) {
5167
+ if (existsSync14(lockFile)) {
5042
5168
  try {
5043
- const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
5169
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
5044
5170
  const age = Date.now() - lock.timestamp;
5045
5171
  if (isProcessAlive(lock.pid) && age < 6e4) {
5046
5172
  return false;
@@ -5048,7 +5174,7 @@ function acquireSpawnLock2(sessionName) {
5048
5174
  } catch {
5049
5175
  }
5050
5176
  }
5051
- writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5177
+ writeFileSync8(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
5052
5178
  return true;
5053
5179
  }
5054
5180
  function releaseSpawnLock2(sessionName) {
@@ -5060,13 +5186,13 @@ function releaseSpawnLock2(sessionName) {
5060
5186
  function resolveBehaviorsExporterScript() {
5061
5187
  try {
5062
5188
  const thisFile = fileURLToPath2(import.meta.url);
5063
- const scriptPath = path16.join(
5064
- path16.dirname(thisFile),
5189
+ const scriptPath = path17.join(
5190
+ path17.dirname(thisFile),
5065
5191
  "..",
5066
5192
  "bin",
5067
5193
  "exe-export-behaviors.js"
5068
5194
  );
5069
- return existsSync12(scriptPath) ? scriptPath : null;
5195
+ return existsSync14(scriptPath) ? scriptPath : null;
5070
5196
  } catch {
5071
5197
  return null;
5072
5198
  }
@@ -5132,12 +5258,12 @@ function extractRootExe(name) {
5132
5258
  return parts.length > 0 ? parts[parts.length - 1] : null;
5133
5259
  }
5134
5260
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5135
- if (!existsSync12(SESSION_CACHE)) {
5261
+ if (!existsSync14(SESSION_CACHE)) {
5136
5262
  mkdirSync6(SESSION_CACHE, { recursive: true });
5137
5263
  }
5138
5264
  const rootExe = extractRootExe(parentExe) ?? parentExe;
5139
- const filePath = path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5140
- writeFileSync7(filePath, JSON.stringify({
5265
+ const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
5266
+ writeFileSync8(filePath, JSON.stringify({
5141
5267
  parentExe: rootExe,
5142
5268
  dispatchedBy: dispatchedBy || rootExe,
5143
5269
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5145,7 +5271,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
5145
5271
  }
5146
5272
  function getParentExe(sessionKey) {
5147
5273
  try {
5148
- const data = JSON.parse(readFileSync11(path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5274
+ const data = JSON.parse(readFileSync12(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
5149
5275
  return data.parentExe || null;
5150
5276
  } catch {
5151
5277
  return null;
@@ -5153,8 +5279,8 @@ function getParentExe(sessionKey) {
5153
5279
  }
5154
5280
  function getDispatchedBy(sessionKey) {
5155
5281
  try {
5156
- const data = JSON.parse(readFileSync11(
5157
- path16.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5282
+ const data = JSON.parse(readFileSync12(
5283
+ path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
5158
5284
  "utf8"
5159
5285
  ));
5160
5286
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -5224,8 +5350,8 @@ async function verifyPaneAtCapacity(sessionName) {
5224
5350
  }
5225
5351
  function readDebounceState() {
5226
5352
  try {
5227
- if (!existsSync12(DEBOUNCE_FILE)) return {};
5228
- const raw = JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
5353
+ if (!existsSync14(DEBOUNCE_FILE)) return {};
5354
+ const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
5229
5355
  const state = {};
5230
5356
  for (const [key, val] of Object.entries(raw)) {
5231
5357
  if (typeof val === "number") {
@@ -5241,8 +5367,8 @@ function readDebounceState() {
5241
5367
  }
5242
5368
  function writeDebounceState(state) {
5243
5369
  try {
5244
- if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
5245
- writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
5370
+ if (!existsSync14(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
5371
+ writeFileSync8(DEBOUNCE_FILE, JSON.stringify(state));
5246
5372
  } catch {
5247
5373
  }
5248
5374
  }
@@ -5340,8 +5466,8 @@ function sendIntercom(targetSession) {
5340
5466
  try {
5341
5467
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5342
5468
  const agent = baseAgentName(rawAgent);
5343
- const markerPath = path16.join(SESSION_CACHE, `current-task-${agent}.json`);
5344
- if (existsSync12(markerPath)) {
5469
+ const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5470
+ if (existsSync14(markerPath)) {
5345
5471
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5346
5472
  return "debounced";
5347
5473
  }
@@ -5350,8 +5476,8 @@ function sendIntercom(targetSession) {
5350
5476
  try {
5351
5477
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
5352
5478
  const agent = baseAgentName(rawAgent);
5353
- const taskDir = path16.join(process.cwd(), "exe", agent);
5354
- if (existsSync12(taskDir)) {
5479
+ const taskDir = path17.join(process.cwd(), "exe", agent);
5480
+ if (existsSync14(taskDir)) {
5355
5481
  const files = readdirSync3(taskDir).filter(
5356
5482
  (f) => f.endsWith(".md") && f !== "DONE.txt"
5357
5483
  );
@@ -5411,6 +5537,21 @@ function notifyParentExe(sessionKey) {
5411
5537
  }
5412
5538
  return true;
5413
5539
  }
5540
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
5541
+ const transport = getTransport();
5542
+ try {
5543
+ const sessions = transport.listSessions();
5544
+ if (!sessions.includes(coordinatorSession)) return false;
5545
+ execSync6(
5546
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5547
+ { timeout: 3e3 }
5548
+ );
5549
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
5550
+ return true;
5551
+ } catch {
5552
+ return false;
5553
+ }
5554
+ }
5414
5555
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
5415
5556
  if (isCoordinatorName(employeeName)) {
5416
5557
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -5484,26 +5625,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5484
5625
  const transport = getTransport();
5485
5626
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
5486
5627
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
5487
- const logDir = path16.join(os9.homedir(), ".exe-os", "session-logs");
5488
- const logFile = path16.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5489
- if (!existsSync12(logDir)) {
5628
+ const logDir = path17.join(os10.homedir(), ".exe-os", "session-logs");
5629
+ const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
5630
+ if (!existsSync14(logDir)) {
5490
5631
  mkdirSync6(logDir, { recursive: true });
5491
5632
  }
5492
5633
  transport.kill(sessionName);
5493
5634
  let cleanupSuffix = "";
5494
5635
  try {
5495
5636
  const thisFile = fileURLToPath2(import.meta.url);
5496
- const cleanupScript = path16.join(path16.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5497
- if (existsSync12(cleanupScript)) {
5637
+ const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
5638
+ if (existsSync14(cleanupScript)) {
5498
5639
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
5499
5640
  }
5500
5641
  } catch {
5501
5642
  }
5502
5643
  try {
5503
- const claudeJsonPath = path16.join(os9.homedir(), ".claude.json");
5644
+ const claudeJsonPath = path17.join(os10.homedir(), ".claude.json");
5504
5645
  let claudeJson = {};
5505
5646
  try {
5506
- claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
5647
+ claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
5507
5648
  } catch {
5508
5649
  }
5509
5650
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -5511,17 +5652,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5511
5652
  const trustDir = opts?.cwd ?? projectDir;
5512
5653
  if (!projects[trustDir]) projects[trustDir] = {};
5513
5654
  projects[trustDir].hasTrustDialogAccepted = true;
5514
- writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5655
+ writeFileSync8(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
5515
5656
  } catch {
5516
5657
  }
5517
5658
  try {
5518
- const settingsDir = path16.join(os9.homedir(), ".claude", "projects");
5659
+ const settingsDir = path17.join(os10.homedir(), ".claude", "projects");
5519
5660
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
5520
- const projSettingsDir = path16.join(settingsDir, normalizedKey);
5521
- const settingsPath = path16.join(projSettingsDir, "settings.json");
5661
+ const projSettingsDir = path17.join(settingsDir, normalizedKey);
5662
+ const settingsPath = path17.join(projSettingsDir, "settings.json");
5522
5663
  let settings = {};
5523
5664
  try {
5524
- settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
5665
+ settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
5525
5666
  } catch {
5526
5667
  }
5527
5668
  const perms = settings.permissions ?? {};
@@ -5550,7 +5691,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5550
5691
  perms.allow = allow;
5551
5692
  settings.permissions = perms;
5552
5693
  mkdirSync6(projSettingsDir, { recursive: true });
5553
- writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5694
+ writeFileSync8(settingsPath, JSON.stringify(settings, null, 2) + "\n");
5554
5695
  }
5555
5696
  } catch {
5556
5697
  }
@@ -5565,8 +5706,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5565
5706
  let behaviorsFlag = "";
5566
5707
  let legacyFallbackWarned = false;
5567
5708
  if (!useExeAgent && !useBinSymlink) {
5568
- const identityPath = path16.join(
5569
- os9.homedir(),
5709
+ const identityPath = path17.join(
5710
+ os10.homedir(),
5570
5711
  ".exe-os",
5571
5712
  "identity",
5572
5713
  `${employeeName}.md`
@@ -5575,13 +5716,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5575
5716
  const hasAgentFlag = claudeSupportsAgentFlag();
5576
5717
  if (hasAgentFlag) {
5577
5718
  identityFlag = ` --agent ${employeeName}`;
5578
- } else if (existsSync12(identityPath)) {
5719
+ } else if (existsSync14(identityPath)) {
5579
5720
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
5580
5721
  legacyFallbackWarned = true;
5581
5722
  }
5582
5723
  const behaviorsFile = exportBehaviorsSync(
5583
5724
  employeeName,
5584
- path16.basename(spawnCwd),
5725
+ path17.basename(spawnCwd),
5585
5726
  sessionName
5586
5727
  );
5587
5728
  if (behaviorsFile) {
@@ -5596,16 +5737,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5596
5737
  }
5597
5738
  let sessionContextFlag = "";
5598
5739
  try {
5599
- const ctxDir = path16.join(os9.homedir(), ".exe-os", "session-cache");
5740
+ const ctxDir = path17.join(os10.homedir(), ".exe-os", "session-cache");
5600
5741
  mkdirSync6(ctxDir, { recursive: true });
5601
- const ctxFile = path16.join(ctxDir, `session-context-${sessionName}.md`);
5742
+ const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
5602
5743
  const ctxContent = [
5603
5744
  `## Session Context`,
5604
5745
  `You are running in tmux session: ${sessionName}.`,
5605
5746
  `Your parent coordinator session is ${exeSession}.`,
5606
5747
  `Your employees (if any) use the -${exeSession} suffix.`
5607
5748
  ].join("\n");
5608
- writeFileSync7(ctxFile, ctxContent);
5749
+ writeFileSync8(ctxFile, ctxContent);
5609
5750
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
5610
5751
  } catch {
5611
5752
  }
@@ -5682,8 +5823,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
5682
5823
  transport.pipeLog(sessionName, logFile);
5683
5824
  try {
5684
5825
  const mySession = getMySession();
5685
- const dispatchInfo = path16.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5686
- writeFileSync7(dispatchInfo, JSON.stringify({
5826
+ const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
5827
+ writeFileSync8(dispatchInfo, JSON.stringify({
5687
5828
  dispatchedBy: mySession,
5688
5829
  rootExe: exeSession,
5689
5830
  provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
@@ -5757,15 +5898,15 @@ var init_tmux_routing = __esm({
5757
5898
  init_intercom_queue();
5758
5899
  init_plan_limits();
5759
5900
  init_employees();
5760
- SPAWN_LOCK_DIR = path16.join(os9.homedir(), ".exe-os", "spawn-locks");
5761
- SESSION_CACHE = path16.join(os9.homedir(), ".exe-os", "session-cache");
5901
+ SPAWN_LOCK_DIR = path17.join(os10.homedir(), ".exe-os", "spawn-locks");
5902
+ SESSION_CACHE = path17.join(os10.homedir(), ".exe-os", "session-cache");
5762
5903
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
5763
5904
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
5764
5905
  VERIFY_PANE_LINES = 200;
5765
5906
  INTERCOM_DEBOUNCE_MS = 3e4;
5766
5907
  CODEX_DEBOUNCE_MS = 12e4;
5767
- INTERCOM_LOG2 = path16.join(os9.homedir(), ".exe-os", "intercom.log");
5768
- DEBOUNCE_FILE = path16.join(SESSION_CACHE, "intercom-debounce.json");
5908
+ INTERCOM_LOG2 = path17.join(os10.homedir(), ".exe-os", "intercom.log");
5909
+ DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
5769
5910
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5770
5911
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
5771
5912
  }
@@ -5788,6 +5929,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
5788
5929
  args: [scope]
5789
5930
  };
5790
5931
  }
5932
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
5933
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
5934
+ if (!scope) return { sql: "", args: [] };
5935
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
5936
+ return {
5937
+ sql: ` AND ${col} = ?`,
5938
+ args: [scope]
5939
+ };
5940
+ }
5791
5941
  var init_task_scope = __esm({
5792
5942
  "src/lib/task-scope.ts"() {
5793
5943
  "use strict";
@@ -5806,14 +5956,14 @@ var init_memory = __esm({
5806
5956
 
5807
5957
  // src/lib/keychain.ts
5808
5958
  import { readFile as readFile4, writeFile as writeFile5, unlink, mkdir as mkdir4, chmod as chmod2 } from "fs/promises";
5809
- import { existsSync as existsSync13 } from "fs";
5810
- import path17 from "path";
5811
- import os10 from "os";
5959
+ import { existsSync as existsSync15 } from "fs";
5960
+ import path18 from "path";
5961
+ import os11 from "os";
5812
5962
  function getKeyDir() {
5813
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path17.join(os10.homedir(), ".exe-os");
5963
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path18.join(os11.homedir(), ".exe-os");
5814
5964
  }
5815
5965
  function getKeyPath() {
5816
- return path17.join(getKeyDir(), "master.key");
5966
+ return path18.join(getKeyDir(), "master.key");
5817
5967
  }
5818
5968
  async function tryKeytar() {
5819
5969
  try {
@@ -5834,9 +5984,9 @@ async function getMasterKey() {
5834
5984
  }
5835
5985
  }
5836
5986
  const keyPath = getKeyPath();
5837
- if (!existsSync13(keyPath)) {
5987
+ if (!existsSync15(keyPath)) {
5838
5988
  process.stderr.write(
5839
- `[keychain] Key not found at ${keyPath} (HOME=${os10.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5989
+ `[keychain] Key not found at ${keyPath} (HOME=${os11.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5840
5990
  `
5841
5991
  );
5842
5992
  return null;
@@ -5866,6 +6016,7 @@ var shard_manager_exports = {};
5866
6016
  __export(shard_manager_exports, {
5867
6017
  disposeShards: () => disposeShards,
5868
6018
  ensureShardSchema: () => ensureShardSchema,
6019
+ getOpenShardCount: () => getOpenShardCount,
5869
6020
  getReadyShardClient: () => getReadyShardClient,
5870
6021
  getShardClient: () => getShardClient,
5871
6022
  getShardsDir: () => getShardsDir,
@@ -5874,15 +6025,18 @@ __export(shard_manager_exports, {
5874
6025
  listShards: () => listShards,
5875
6026
  shardExists: () => shardExists
5876
6027
  });
5877
- import path18 from "path";
5878
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
6028
+ import path19 from "path";
6029
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7, readdirSync as readdirSync4 } from "fs";
5879
6030
  import { createClient as createClient2 } from "@libsql/client";
5880
6031
  function initShardManager(encryptionKey) {
5881
6032
  _encryptionKey = encryptionKey;
5882
- if (!existsSync14(SHARDS_DIR)) {
6033
+ if (!existsSync16(SHARDS_DIR)) {
5883
6034
  mkdirSync7(SHARDS_DIR, { recursive: true });
5884
6035
  }
5885
6036
  _shardingEnabled = true;
6037
+ if (_evictionTimer) clearInterval(_evictionTimer);
6038
+ _evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
6039
+ _evictionTimer.unref();
5886
6040
  }
5887
6041
  function isShardingEnabled() {
5888
6042
  return _shardingEnabled;
@@ -5899,21 +6053,28 @@ function getShardClient(projectName) {
5899
6053
  throw new Error(`Invalid project name for shard: "${projectName}"`);
5900
6054
  }
5901
6055
  const cached = _shards.get(safeName);
5902
- if (cached) return cached;
5903
- const dbPath = path18.join(SHARDS_DIR, `${safeName}.db`);
6056
+ if (cached) {
6057
+ _shardLastAccess.set(safeName, Date.now());
6058
+ return cached;
6059
+ }
6060
+ while (_shards.size >= MAX_OPEN_SHARDS) {
6061
+ evictLRU();
6062
+ }
6063
+ const dbPath = path19.join(SHARDS_DIR, `${safeName}.db`);
5904
6064
  const client = createClient2({
5905
6065
  url: `file:${dbPath}`,
5906
6066
  encryptionKey: _encryptionKey
5907
6067
  });
5908
6068
  _shards.set(safeName, client);
6069
+ _shardLastAccess.set(safeName, Date.now());
5909
6070
  return client;
5910
6071
  }
5911
6072
  function shardExists(projectName) {
5912
6073
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
5913
- return existsSync14(path18.join(SHARDS_DIR, `${safeName}.db`));
6074
+ return existsSync16(path19.join(SHARDS_DIR, `${safeName}.db`));
5914
6075
  }
5915
6076
  function listShards() {
5916
- if (!existsSync14(SHARDS_DIR)) return [];
6077
+ if (!existsSync16(SHARDS_DIR)) return [];
5917
6078
  return readdirSync4(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
5918
6079
  }
5919
6080
  async function ensureShardSchema(client) {
@@ -5965,6 +6126,8 @@ async function ensureShardSchema(client) {
5965
6126
  for (const col of [
5966
6127
  "ALTER TABLE memories ADD COLUMN task_id TEXT",
5967
6128
  "ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
6129
+ "ALTER TABLE memories ADD COLUMN author_device_id TEXT",
6130
+ "ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
5968
6131
  "ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
5969
6132
  "ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
5970
6133
  "ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
@@ -6102,21 +6265,69 @@ async function getReadyShardClient(projectName) {
6102
6265
  await ensureShardSchema(client);
6103
6266
  return client;
6104
6267
  }
6268
+ function evictLRU() {
6269
+ let oldest = null;
6270
+ let oldestTime = Infinity;
6271
+ for (const [name, time] of _shardLastAccess) {
6272
+ if (time < oldestTime) {
6273
+ oldestTime = time;
6274
+ oldest = name;
6275
+ }
6276
+ }
6277
+ if (oldest) {
6278
+ const client = _shards.get(oldest);
6279
+ if (client) {
6280
+ client.close();
6281
+ }
6282
+ _shards.delete(oldest);
6283
+ _shardLastAccess.delete(oldest);
6284
+ }
6285
+ }
6286
+ function evictIdleShards() {
6287
+ const now = Date.now();
6288
+ const toEvict = [];
6289
+ for (const [name, lastAccess] of _shardLastAccess) {
6290
+ if (now - lastAccess > SHARD_IDLE_MS) {
6291
+ toEvict.push(name);
6292
+ }
6293
+ }
6294
+ for (const name of toEvict) {
6295
+ const client = _shards.get(name);
6296
+ if (client) {
6297
+ client.close();
6298
+ }
6299
+ _shards.delete(name);
6300
+ _shardLastAccess.delete(name);
6301
+ }
6302
+ }
6303
+ function getOpenShardCount() {
6304
+ return _shards.size;
6305
+ }
6105
6306
  function disposeShards() {
6307
+ if (_evictionTimer) {
6308
+ clearInterval(_evictionTimer);
6309
+ _evictionTimer = null;
6310
+ }
6106
6311
  for (const [, client] of _shards) {
6107
6312
  client.close();
6108
6313
  }
6109
6314
  _shards.clear();
6315
+ _shardLastAccess.clear();
6110
6316
  _shardingEnabled = false;
6111
6317
  _encryptionKey = null;
6112
6318
  }
6113
- var SHARDS_DIR, _shards, _encryptionKey, _shardingEnabled;
6319
+ var SHARDS_DIR, SHARD_IDLE_MS, MAX_OPEN_SHARDS, EVICTION_INTERVAL_MS, _shards, _shardLastAccess, _evictionTimer, _encryptionKey, _shardingEnabled;
6114
6320
  var init_shard_manager = __esm({
6115
6321
  "src/lib/shard-manager.ts"() {
6116
6322
  "use strict";
6117
6323
  init_config();
6118
- SHARDS_DIR = path18.join(EXE_AI_DIR, "shards");
6324
+ SHARDS_DIR = path19.join(EXE_AI_DIR, "shards");
6325
+ SHARD_IDLE_MS = 5 * 60 * 1e3;
6326
+ MAX_OPEN_SHARDS = 10;
6327
+ EVICTION_INTERVAL_MS = 60 * 1e3;
6119
6328
  _shards = /* @__PURE__ */ new Map();
6329
+ _shardLastAccess = /* @__PURE__ */ new Map();
6330
+ _evictionTimer = null;
6120
6331
  _encryptionKey = null;
6121
6332
  _shardingEnabled = false;
6122
6333
  }