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