@askexenow/exe-os 0.9.8 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/bin/backfill-conversations.js +222 -49
  2. package/dist/bin/backfill-responses.js +221 -48
  3. package/dist/bin/backfill-vectors.js +225 -52
  4. package/dist/bin/cleanup-stale-review-tasks.js +150 -28
  5. package/dist/bin/cli.js +1295 -856
  6. package/dist/bin/exe-agent-config.js +36 -8
  7. package/dist/bin/exe-agent.js +14 -4
  8. package/dist/bin/exe-assign.js +221 -48
  9. package/dist/bin/exe-boot.js +778 -427
  10. package/dist/bin/exe-call.js +41 -13
  11. package/dist/bin/exe-cloud.js +163 -58
  12. package/dist/bin/exe-dispatch.js +276 -139
  13. package/dist/bin/exe-doctor.js +145 -27
  14. package/dist/bin/exe-export-behaviors.js +141 -23
  15. package/dist/bin/exe-forget.js +137 -19
  16. package/dist/bin/exe-gateway.js +677 -388
  17. package/dist/bin/exe-heartbeat.js +227 -108
  18. package/dist/bin/exe-kill.js +138 -20
  19. package/dist/bin/exe-launch-agent.js +172 -39
  20. package/dist/bin/exe-link.js +291 -100
  21. package/dist/bin/exe-new-employee.js +214 -106
  22. package/dist/bin/exe-pending-messages.js +395 -33
  23. package/dist/bin/exe-pending-notifications.js +684 -99
  24. package/dist/bin/exe-pending-reviews.js +420 -74
  25. package/dist/bin/exe-rename.js +147 -49
  26. package/dist/bin/exe-review.js +138 -20
  27. package/dist/bin/exe-search.js +240 -69
  28. package/dist/bin/exe-session-cleanup.js +440 -250
  29. package/dist/bin/exe-settings.js +61 -17
  30. package/dist/bin/exe-start-codex.js +158 -39
  31. package/dist/bin/exe-start-opencode.js +157 -38
  32. package/dist/bin/exe-status.js +151 -29
  33. package/dist/bin/exe-team.js +138 -20
  34. package/dist/bin/git-sweep.js +404 -212
  35. package/dist/bin/graph-backfill.js +137 -19
  36. package/dist/bin/graph-export.js +140 -22
  37. package/dist/bin/install.js +90 -61
  38. package/dist/bin/scan-tasks.js +412 -220
  39. package/dist/bin/setup.js +564 -293
  40. package/dist/bin/shard-migrate.js +139 -21
  41. package/dist/bin/update.js +138 -49
  42. package/dist/bin/wiki-sync.js +137 -19
  43. package/dist/gateway/index.js +533 -320
  44. package/dist/hooks/bug-report-worker.js +344 -193
  45. package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
  46. package/dist/hooks/commit-complete.js +402 -210
  47. package/dist/hooks/error-recall.js +245 -74
  48. package/dist/hooks/exe-heartbeat-hook.js +16 -6
  49. package/dist/hooks/ingest-worker.js +3423 -3157
  50. package/dist/hooks/ingest.js +832 -97
  51. package/dist/hooks/instructions-loaded.js +227 -54
  52. package/dist/hooks/notification.js +216 -43
  53. package/dist/hooks/post-compact.js +239 -62
  54. package/dist/hooks/pre-compact.js +408 -216
  55. package/dist/hooks/pre-tool-use.js +268 -90
  56. package/dist/hooks/prompt-ingest-worker.js +352 -102
  57. package/dist/hooks/prompt-submit.js +541 -328
  58. package/dist/hooks/response-ingest-worker.js +372 -122
  59. package/dist/hooks/session-end.js +443 -240
  60. package/dist/hooks/session-start.js +313 -127
  61. package/dist/hooks/stop.js +293 -98
  62. package/dist/hooks/subagent-stop.js +239 -62
  63. package/dist/hooks/summary-worker.js +568 -236
  64. package/dist/index.js +538 -324
  65. package/dist/lib/agent-config.js +28 -6
  66. package/dist/lib/cloud-sync.js +284 -105
  67. package/dist/lib/config.js +30 -10
  68. package/dist/lib/consolidation.js +16 -6
  69. package/dist/lib/database.js +123 -25
  70. package/dist/lib/db-daemon-client.js +73 -19
  71. package/dist/lib/db.js +123 -25
  72. package/dist/lib/device-registry.js +133 -35
  73. package/dist/lib/embedder.js +107 -32
  74. package/dist/lib/employee-templates.js +14 -4
  75. package/dist/lib/employees.js +41 -13
  76. package/dist/lib/exe-daemon-client.js +88 -22
  77. package/dist/lib/exe-daemon.js +935 -587
  78. package/dist/lib/hybrid-search.js +240 -69
  79. package/dist/lib/identity.js +18 -8
  80. package/dist/lib/license.js +133 -48
  81. package/dist/lib/messaging.js +116 -56
  82. package/dist/lib/reminders.js +14 -4
  83. package/dist/lib/schedules.js +137 -19
  84. package/dist/lib/skill-learning.js +33 -6
  85. package/dist/lib/store.js +137 -19
  86. package/dist/lib/task-router.js +14 -4
  87. package/dist/lib/tasks.js +280 -234
  88. package/dist/lib/tmux-routing.js +172 -125
  89. package/dist/lib/token-spend.js +26 -8
  90. package/dist/mcp/server.js +1326 -609
  91. package/dist/mcp/tools/complete-reminder.js +14 -4
  92. package/dist/mcp/tools/create-reminder.js +14 -4
  93. package/dist/mcp/tools/create-task.js +306 -248
  94. package/dist/mcp/tools/deactivate-behavior.js +16 -6
  95. package/dist/mcp/tools/list-reminders.js +14 -4
  96. package/dist/mcp/tools/list-tasks.js +123 -107
  97. package/dist/mcp/tools/send-message.js +75 -29
  98. package/dist/mcp/tools/update-task.js +1848 -199
  99. package/dist/runtime/index.js +441 -248
  100. package/dist/tui/App.js +761 -424
  101. package/package.json +1 -1
@@ -80,9 +80,47 @@ var init_db_retry = __esm({
80
80
  }
81
81
  });
82
82
 
83
+ // src/lib/secure-files.ts
84
+ import { chmodSync, existsSync, mkdirSync } from "fs";
85
+ import { chmod, mkdir } from "fs/promises";
86
+ async function ensurePrivateDir(dirPath) {
87
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
88
+ try {
89
+ await chmod(dirPath, PRIVATE_DIR_MODE);
90
+ } catch {
91
+ }
92
+ }
93
+ function ensurePrivateDirSync(dirPath) {
94
+ mkdirSync(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
95
+ try {
96
+ chmodSync(dirPath, PRIVATE_DIR_MODE);
97
+ } catch {
98
+ }
99
+ }
100
+ async function enforcePrivateFile(filePath) {
101
+ try {
102
+ await chmod(filePath, PRIVATE_FILE_MODE);
103
+ } catch {
104
+ }
105
+ }
106
+ function enforcePrivateFileSync(filePath) {
107
+ try {
108
+ if (existsSync(filePath)) chmodSync(filePath, PRIVATE_FILE_MODE);
109
+ } catch {
110
+ }
111
+ }
112
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
113
+ var init_secure_files = __esm({
114
+ "src/lib/secure-files.ts"() {
115
+ "use strict";
116
+ PRIVATE_DIR_MODE = 448;
117
+ PRIVATE_FILE_MODE = 384;
118
+ }
119
+ });
120
+
83
121
  // src/lib/config.ts
84
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
85
- import { readFileSync, existsSync, renameSync } from "fs";
122
+ import { readFile, writeFile } from "fs/promises";
123
+ import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
86
124
  import path from "path";
87
125
  import os from "os";
88
126
  function resolveDataDir() {
@@ -90,7 +128,7 @@ function resolveDataDir() {
90
128
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
91
129
  const newDir = path.join(os.homedir(), ".exe-os");
92
130
  const legacyDir = path.join(os.homedir(), ".exe-mem");
93
- if (!existsSync(newDir) && existsSync(legacyDir)) {
131
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
94
132
  try {
95
133
  renameSync(legacyDir, newDir);
96
134
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -153,9 +191,9 @@ function normalizeAutoUpdate(raw) {
153
191
  }
154
192
  async function loadConfig() {
155
193
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
156
- await mkdir(dir, { recursive: true });
194
+ await ensurePrivateDir(dir);
157
195
  const configPath = path.join(dir, "config.json");
158
- if (!existsSync(configPath)) {
196
+ if (!existsSync2(configPath)) {
159
197
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
160
198
  }
161
199
  const raw = await readFile(configPath, "utf-8");
@@ -168,6 +206,7 @@ async function loadConfig() {
168
206
  `);
169
207
  try {
170
208
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
209
+ await enforcePrivateFile(configPath);
171
210
  } catch {
172
211
  }
173
212
  }
@@ -187,6 +226,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
187
226
  var init_config = __esm({
188
227
  "src/lib/config.ts"() {
189
228
  "use strict";
229
+ init_secure_files();
190
230
  EXE_AI_DIR = resolveDataDir();
191
231
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
192
232
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
@@ -303,10 +343,10 @@ __export(agent_config_exports, {
303
343
  saveAgentConfig: () => saveAgentConfig,
304
344
  setAgentRuntime: () => setAgentRuntime
305
345
  });
306
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync } from "fs";
346
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
307
347
  import path2 from "path";
308
348
  function loadAgentConfig() {
309
- if (!existsSync2(AGENT_CONFIG_PATH)) return {};
349
+ if (!existsSync3(AGENT_CONFIG_PATH)) return {};
310
350
  try {
311
351
  return JSON.parse(readFileSync2(AGENT_CONFIG_PATH, "utf-8"));
312
352
  } catch {
@@ -315,8 +355,9 @@ function loadAgentConfig() {
315
355
  }
316
356
  function saveAgentConfig(config) {
317
357
  const dir = path2.dirname(AGENT_CONFIG_PATH);
318
- if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
358
+ ensurePrivateDirSync(dir);
319
359
  writeFileSync(AGENT_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
360
+ enforcePrivateFileSync(AGENT_CONFIG_PATH);
320
361
  }
321
362
  function getAgentRuntime(agentId) {
322
363
  const config = loadAgentConfig();
@@ -356,6 +397,7 @@ var init_agent_config = __esm({
356
397
  "use strict";
357
398
  init_config();
358
399
  init_runtime_table();
400
+ init_secure_files();
359
401
  AGENT_CONFIG_PATH = path2.join(EXE_AI_DIR, "agent-config.json");
360
402
  KNOWN_RUNTIMES = {
361
403
  claude: ["claude-opus-4", "claude-sonnet-4", "claude-haiku-4.5"],
@@ -403,7 +445,7 @@ __export(employees_exports, {
403
445
  validateEmployeeName: () => validateEmployeeName
404
446
  });
405
447
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
406
- import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
448
+ import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
407
449
  import { execSync } from "child_process";
408
450
  import path3 from "path";
409
451
  import os2 from "os";
@@ -442,7 +484,7 @@ function validateEmployeeName(name) {
442
484
  return { valid: true };
443
485
  }
444
486
  async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
445
- if (!existsSync3(employeesPath)) {
487
+ if (!existsSync4(employeesPath)) {
446
488
  return [];
447
489
  }
448
490
  const raw = await readFile2(employeesPath, "utf-8");
@@ -457,7 +499,7 @@ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
457
499
  await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
458
500
  }
459
501
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
460
- if (!existsSync3(employeesPath)) return [];
502
+ if (!existsSync4(employeesPath)) return [];
461
503
  try {
462
504
  return JSON.parse(readFileSync3(employeesPath, "utf-8"));
463
505
  } catch {
@@ -505,7 +547,7 @@ function appendToCoordinatorTeam(employee) {
505
547
  const coordinator = getCoordinatorEmployee(loadEmployeesSync());
506
548
  if (!coordinator) return;
507
549
  const idPath = path3.join(IDENTITY_DIR, `${coordinator.name}.md`);
508
- if (!existsSync3(idPath)) return;
550
+ if (!existsSync4(idPath)) return;
509
551
  const content = readFileSync3(idPath, "utf-8");
510
552
  if (content.includes(`**${capitalize(employee.name)}`)) return;
511
553
  const teamMatch = content.match(TEAM_SECTION_RE);
@@ -559,9 +601,9 @@ async function normalizeRosterCase(rosterPath) {
559
601
  const identityDir = path3.join(os2.homedir(), ".exe-os", "identity");
560
602
  const oldPath = path3.join(identityDir, `${oldName}.md`);
561
603
  const newPath = path3.join(identityDir, `${emp.name}.md`);
562
- if (existsSync3(oldPath) && !existsSync3(newPath)) {
604
+ if (existsSync4(oldPath) && !existsSync4(newPath)) {
563
605
  renameSync2(oldPath, newPath);
564
- } else if (existsSync3(oldPath) && oldPath !== newPath) {
606
+ } else if (existsSync4(oldPath) && oldPath !== newPath) {
565
607
  const content = readFileSync3(oldPath, "utf-8");
566
608
  writeFileSync2(newPath, content, "utf-8");
567
609
  if (oldPath.toLowerCase() !== newPath.toLowerCase()) {
@@ -604,7 +646,7 @@ function registerBinSymlinks(name) {
604
646
  for (const suffix of ["", "-opencode"]) {
605
647
  const linkName = `${name}${suffix}`;
606
648
  const linkPath = path3.join(binDir, linkName);
607
- if (existsSync3(linkPath)) {
649
+ if (existsSync4(linkPath)) {
608
650
  skipped.push(linkName);
609
651
  continue;
610
652
  }
@@ -1557,6 +1599,7 @@ async function ensureSchema() {
1557
1599
  project TEXT NOT NULL,
1558
1600
  summary TEXT NOT NULL,
1559
1601
  task_file TEXT,
1602
+ session_scope TEXT,
1560
1603
  read INTEGER NOT NULL DEFAULT 0,
1561
1604
  created_at TEXT NOT NULL
1562
1605
  );
@@ -1565,7 +1608,7 @@ async function ensureSchema() {
1565
1608
  ON notifications(read);
1566
1609
 
1567
1610
  CREATE INDEX IF NOT EXISTS idx_notifications_agent
1568
- ON notifications(agent_id);
1611
+ ON notifications(agent_id, session_scope);
1569
1612
 
1570
1613
  CREATE INDEX IF NOT EXISTS idx_notifications_task_file
1571
1614
  ON notifications(task_file);
@@ -1603,6 +1646,7 @@ async function ensureSchema() {
1603
1646
  target_agent TEXT NOT NULL,
1604
1647
  target_project TEXT,
1605
1648
  target_device TEXT NOT NULL DEFAULT 'local',
1649
+ session_scope TEXT,
1606
1650
  content TEXT NOT NULL,
1607
1651
  priority TEXT DEFAULT 'normal',
1608
1652
  status TEXT DEFAULT 'pending',
@@ -1616,10 +1660,31 @@ async function ensureSchema() {
1616
1660
  );
1617
1661
 
1618
1662
  CREATE INDEX IF NOT EXISTS idx_messages_target
1619
- ON messages(target_agent, status);
1663
+ ON messages(target_agent, session_scope, status);
1620
1664
 
1621
1665
  CREATE INDEX IF NOT EXISTS idx_messages_conversation_order
1622
- ON messages(target_agent, from_agent, server_seq);
1666
+ ON messages(target_agent, session_scope, from_agent, server_seq);
1667
+ `);
1668
+ try {
1669
+ await client.execute({
1670
+ sql: `ALTER TABLE notifications ADD COLUMN session_scope TEXT`,
1671
+ args: []
1672
+ });
1673
+ } catch {
1674
+ }
1675
+ try {
1676
+ await client.execute({
1677
+ sql: `ALTER TABLE messages ADD COLUMN session_scope TEXT`,
1678
+ args: []
1679
+ });
1680
+ } catch {
1681
+ }
1682
+ await client.executeMultiple(`
1683
+ CREATE INDEX IF NOT EXISTS idx_notifications_agent_scope_read
1684
+ ON notifications(agent_id, session_scope, read, created_at);
1685
+
1686
+ CREATE INDEX IF NOT EXISTS idx_messages_target_scope_status
1687
+ ON messages(target_agent, session_scope, status, created_at);
1623
1688
  `);
1624
1689
  try {
1625
1690
  await client.execute({
@@ -2203,6 +2268,13 @@ async function ensureSchema() {
2203
2268
  } catch {
2204
2269
  }
2205
2270
  }
2271
+ try {
2272
+ await client.execute({
2273
+ sql: `UPDATE tasks SET status = 'closed' WHERE status = 'done' AND result IS NOT NULL`,
2274
+ args: []
2275
+ });
2276
+ } catch {
2277
+ }
2206
2278
  }
2207
2279
  var _client, _resilientClient, _walCheckpointTimer, _daemonClient, _adapterClient, initTurso;
2208
2280
  var init_database = __esm({
@@ -2280,6 +2352,7 @@ var shard_manager_exports = {};
2280
2352
  __export(shard_manager_exports, {
2281
2353
  disposeShards: () => disposeShards,
2282
2354
  ensureShardSchema: () => ensureShardSchema,
2355
+ getOpenShardCount: () => getOpenShardCount,
2283
2356
  getReadyShardClient: () => getReadyShardClient,
2284
2357
  getShardClient: () => getShardClient,
2285
2358
  getShardsDir: () => getShardsDir,
@@ -2289,14 +2362,17 @@ __export(shard_manager_exports, {
2289
2362
  shardExists: () => shardExists
2290
2363
  });
2291
2364
  import path6 from "path";
2292
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync } from "fs";
2365
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync } from "fs";
2293
2366
  import { createClient as createClient2 } from "@libsql/client";
2294
2367
  function initShardManager(encryptionKey) {
2295
2368
  _encryptionKey = encryptionKey;
2296
- if (!existsSync5(SHARDS_DIR)) {
2369
+ if (!existsSync6(SHARDS_DIR)) {
2297
2370
  mkdirSync2(SHARDS_DIR, { recursive: true });
2298
2371
  }
2299
2372
  _shardingEnabled = true;
2373
+ if (_evictionTimer) clearInterval(_evictionTimer);
2374
+ _evictionTimer = setInterval(evictIdleShards, EVICTION_INTERVAL_MS);
2375
+ _evictionTimer.unref();
2300
2376
  }
2301
2377
  function isShardingEnabled() {
2302
2378
  return _shardingEnabled;
@@ -2313,21 +2389,28 @@ function getShardClient(projectName) {
2313
2389
  throw new Error(`Invalid project name for shard: "${projectName}"`);
2314
2390
  }
2315
2391
  const cached = _shards.get(safeName);
2316
- if (cached) return cached;
2392
+ if (cached) {
2393
+ _shardLastAccess.set(safeName, Date.now());
2394
+ return cached;
2395
+ }
2396
+ while (_shards.size >= MAX_OPEN_SHARDS) {
2397
+ evictLRU();
2398
+ }
2317
2399
  const dbPath = path6.join(SHARDS_DIR, `${safeName}.db`);
2318
2400
  const client = createClient2({
2319
2401
  url: `file:${dbPath}`,
2320
2402
  encryptionKey: _encryptionKey
2321
2403
  });
2322
2404
  _shards.set(safeName, client);
2405
+ _shardLastAccess.set(safeName, Date.now());
2323
2406
  return client;
2324
2407
  }
2325
2408
  function shardExists(projectName) {
2326
2409
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
2327
- return existsSync5(path6.join(SHARDS_DIR, `${safeName}.db`));
2410
+ return existsSync6(path6.join(SHARDS_DIR, `${safeName}.db`));
2328
2411
  }
2329
2412
  function listShards() {
2330
- if (!existsSync5(SHARDS_DIR)) return [];
2413
+ if (!existsSync6(SHARDS_DIR)) return [];
2331
2414
  return readdirSync(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
2332
2415
  }
2333
2416
  async function ensureShardSchema(client) {
@@ -2379,6 +2462,8 @@ async function ensureShardSchema(client) {
2379
2462
  for (const col of [
2380
2463
  "ALTER TABLE memories ADD COLUMN task_id TEXT",
2381
2464
  "ALTER TABLE memories ADD COLUMN consolidated INTEGER NOT NULL DEFAULT 0",
2465
+ "ALTER TABLE memories ADD COLUMN author_device_id TEXT",
2466
+ "ALTER TABLE memories ADD COLUMN scope TEXT NOT NULL DEFAULT 'business'",
2382
2467
  "ALTER TABLE memories ADD COLUMN importance INTEGER DEFAULT 5",
2383
2468
  "ALTER TABLE memories ADD COLUMN status TEXT DEFAULT 'active'",
2384
2469
  "ALTER TABLE memories ADD COLUMN wiki_synced INTEGER DEFAULT 0",
@@ -2516,21 +2601,69 @@ async function getReadyShardClient(projectName) {
2516
2601
  await ensureShardSchema(client);
2517
2602
  return client;
2518
2603
  }
2604
+ function evictLRU() {
2605
+ let oldest = null;
2606
+ let oldestTime = Infinity;
2607
+ for (const [name, time] of _shardLastAccess) {
2608
+ if (time < oldestTime) {
2609
+ oldestTime = time;
2610
+ oldest = name;
2611
+ }
2612
+ }
2613
+ if (oldest) {
2614
+ const client = _shards.get(oldest);
2615
+ if (client) {
2616
+ client.close();
2617
+ }
2618
+ _shards.delete(oldest);
2619
+ _shardLastAccess.delete(oldest);
2620
+ }
2621
+ }
2622
+ function evictIdleShards() {
2623
+ const now = Date.now();
2624
+ const toEvict = [];
2625
+ for (const [name, lastAccess] of _shardLastAccess) {
2626
+ if (now - lastAccess > SHARD_IDLE_MS) {
2627
+ toEvict.push(name);
2628
+ }
2629
+ }
2630
+ for (const name of toEvict) {
2631
+ const client = _shards.get(name);
2632
+ if (client) {
2633
+ client.close();
2634
+ }
2635
+ _shards.delete(name);
2636
+ _shardLastAccess.delete(name);
2637
+ }
2638
+ }
2639
+ function getOpenShardCount() {
2640
+ return _shards.size;
2641
+ }
2519
2642
  function disposeShards() {
2643
+ if (_evictionTimer) {
2644
+ clearInterval(_evictionTimer);
2645
+ _evictionTimer = null;
2646
+ }
2520
2647
  for (const [, client] of _shards) {
2521
2648
  client.close();
2522
2649
  }
2523
2650
  _shards.clear();
2651
+ _shardLastAccess.clear();
2524
2652
  _shardingEnabled = false;
2525
2653
  _encryptionKey = null;
2526
2654
  }
2527
- var SHARDS_DIR, _shards, _encryptionKey, _shardingEnabled;
2655
+ var SHARDS_DIR, SHARD_IDLE_MS, MAX_OPEN_SHARDS, EVICTION_INTERVAL_MS, _shards, _shardLastAccess, _evictionTimer, _encryptionKey, _shardingEnabled;
2528
2656
  var init_shard_manager = __esm({
2529
2657
  "src/lib/shard-manager.ts"() {
2530
2658
  "use strict";
2531
2659
  init_config();
2532
2660
  SHARDS_DIR = path6.join(EXE_AI_DIR, "shards");
2661
+ SHARD_IDLE_MS = 5 * 60 * 1e3;
2662
+ MAX_OPEN_SHARDS = 10;
2663
+ EVICTION_INTERVAL_MS = 60 * 1e3;
2533
2664
  _shards = /* @__PURE__ */ new Map();
2665
+ _shardLastAccess = /* @__PURE__ */ new Map();
2666
+ _evictionTimer = null;
2534
2667
  _encryptionKey = null;
2535
2668
  _shardingEnabled = false;
2536
2669
  }
@@ -2723,64 +2856,12 @@ ${p.content}`).join("\n\n");
2723
2856
  }
2724
2857
  });
2725
2858
 
2726
- // src/lib/notifications.ts
2727
- import crypto from "crypto";
2859
+ // src/lib/session-registry.ts
2860
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "fs";
2728
2861
  import path7 from "path";
2729
2862
  import os5 from "os";
2730
- import {
2731
- readFileSync as readFileSync4,
2732
- readdirSync as readdirSync2,
2733
- unlinkSync as unlinkSync2,
2734
- existsSync as existsSync6,
2735
- rmdirSync
2736
- } from "fs";
2737
- async function writeNotification(notification) {
2738
- try {
2739
- const client = getClient();
2740
- const id = crypto.randomUUID();
2741
- const now = (/* @__PURE__ */ new Date()).toISOString();
2742
- await client.execute({
2743
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
2744
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2745
- args: [
2746
- id,
2747
- notification.agentId,
2748
- notification.agentRole,
2749
- notification.event,
2750
- notification.project,
2751
- notification.summary,
2752
- notification.taskFile ?? null,
2753
- now
2754
- ]
2755
- });
2756
- } catch (err) {
2757
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
2758
- `);
2759
- }
2760
- }
2761
- async function markAsReadByTaskFile(taskFile) {
2762
- try {
2763
- const client = getClient();
2764
- await client.execute({
2765
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
2766
- args: [taskFile]
2767
- });
2768
- } catch {
2769
- }
2770
- }
2771
- var init_notifications = __esm({
2772
- "src/lib/notifications.ts"() {
2773
- "use strict";
2774
- init_database();
2775
- }
2776
- });
2777
-
2778
- // src/lib/session-registry.ts
2779
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync7 } from "fs";
2780
- import path8 from "path";
2781
- import os6 from "os";
2782
2863
  function registerSession(entry) {
2783
- const dir = path8.dirname(REGISTRY_PATH);
2864
+ const dir = path7.dirname(REGISTRY_PATH);
2784
2865
  if (!existsSync7(dir)) {
2785
2866
  mkdirSync3(dir, { recursive: true });
2786
2867
  }
@@ -2795,7 +2876,7 @@ function registerSession(entry) {
2795
2876
  }
2796
2877
  function listSessions() {
2797
2878
  try {
2798
- const raw = readFileSync5(REGISTRY_PATH, "utf8");
2879
+ const raw = readFileSync4(REGISTRY_PATH, "utf8");
2799
2880
  return JSON.parse(raw);
2800
2881
  } catch {
2801
2882
  return [];
@@ -2805,7 +2886,7 @@ var REGISTRY_PATH;
2805
2886
  var init_session_registry = __esm({
2806
2887
  "src/lib/session-registry.ts"() {
2807
2888
  "use strict";
2808
- REGISTRY_PATH = path8.join(os6.homedir(), ".exe-os", "session-registry.json");
2889
+ REGISTRY_PATH = path7.join(os5.homedir(), ".exe-os", "session-registry.json");
2809
2890
  }
2810
2891
  });
2811
2892
 
@@ -3066,17 +3147,17 @@ __export(intercom_queue_exports, {
3066
3147
  queueIntercom: () => queueIntercom,
3067
3148
  readQueue: () => readQueue
3068
3149
  });
3069
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
3070
- import path9 from "path";
3071
- import os7 from "os";
3150
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
3151
+ import path8 from "path";
3152
+ import os6 from "os";
3072
3153
  function ensureDir() {
3073
- const dir = path9.dirname(QUEUE_PATH);
3154
+ const dir = path8.dirname(QUEUE_PATH);
3074
3155
  if (!existsSync8(dir)) mkdirSync4(dir, { recursive: true });
3075
3156
  }
3076
3157
  function readQueue() {
3077
3158
  try {
3078
3159
  if (!existsSync8(QUEUE_PATH)) return [];
3079
- return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
3160
+ return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
3080
3161
  } catch {
3081
3162
  return [];
3082
3163
  }
@@ -3176,26 +3257,29 @@ var QUEUE_PATH, MAX_RETRIES2, TTL_MS, INTERCOM_LOG;
3176
3257
  var init_intercom_queue = __esm({
3177
3258
  "src/lib/intercom-queue.ts"() {
3178
3259
  "use strict";
3179
- QUEUE_PATH = path9.join(os7.homedir(), ".exe-os", "intercom-queue.json");
3260
+ QUEUE_PATH = path8.join(os6.homedir(), ".exe-os", "intercom-queue.json");
3180
3261
  MAX_RETRIES2 = 5;
3181
3262
  TTL_MS = 60 * 60 * 1e3;
3182
- INTERCOM_LOG = path9.join(os7.homedir(), ".exe-os", "intercom.log");
3263
+ INTERCOM_LOG = path8.join(os6.homedir(), ".exe-os", "intercom.log");
3183
3264
  }
3184
3265
  });
3185
3266
 
3186
3267
  // src/lib/license.ts
3187
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
3268
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
3188
3269
  import { randomUUID as randomUUID2 } from "crypto";
3189
- import path10 from "path";
3270
+ import { createRequire as createRequire2 } from "module";
3271
+ import { pathToFileURL as pathToFileURL2 } from "url";
3272
+ import os7 from "os";
3273
+ import path9 from "path";
3190
3274
  import { jwtVerify, importSPKI } from "jose";
3191
3275
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
3192
3276
  var init_license = __esm({
3193
3277
  "src/lib/license.ts"() {
3194
3278
  "use strict";
3195
3279
  init_config();
3196
- LICENSE_PATH = path10.join(EXE_AI_DIR, "license.key");
3197
- CACHE_PATH = path10.join(EXE_AI_DIR, "license-cache.json");
3198
- DEVICE_ID_PATH = path10.join(EXE_AI_DIR, "device-id");
3280
+ LICENSE_PATH = path9.join(EXE_AI_DIR, "license.key");
3281
+ CACHE_PATH = path9.join(EXE_AI_DIR, "license-cache.json");
3282
+ DEVICE_ID_PATH = path9.join(EXE_AI_DIR, "device-id");
3199
3283
  PLAN_LIMITS = {
3200
3284
  free: { devices: 1, employees: 1, memories: 5e3 },
3201
3285
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -3207,12 +3291,12 @@ var init_license = __esm({
3207
3291
  });
3208
3292
 
3209
3293
  // src/lib/plan-limits.ts
3210
- import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
3211
- import path11 from "path";
3294
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
3295
+ import path10 from "path";
3212
3296
  function getLicenseSync() {
3213
3297
  try {
3214
3298
  if (!existsSync10(CACHE_PATH2)) return freeLicense();
3215
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
3299
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
3216
3300
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
3217
3301
  const parts = raw.token.split(".");
3218
3302
  if (parts.length !== 3) return freeLicense();
@@ -3251,7 +3335,7 @@ function assertEmployeeLimitSync(rosterPath) {
3251
3335
  let count = 0;
3252
3336
  try {
3253
3337
  if (existsSync10(filePath)) {
3254
- const raw = readFileSync8(filePath, "utf8");
3338
+ const raw = readFileSync7(filePath, "utf8");
3255
3339
  const employees = JSON.parse(raw);
3256
3340
  count = Array.isArray(employees) ? employees.length : 0;
3257
3341
  }
@@ -3280,12 +3364,12 @@ var init_plan_limits = __esm({
3280
3364
  this.name = "PlanLimitError";
3281
3365
  }
3282
3366
  };
3283
- CACHE_PATH2 = path11.join(EXE_AI_DIR, "license-cache.json");
3367
+ CACHE_PATH2 = path10.join(EXE_AI_DIR, "license-cache.json");
3284
3368
  }
3285
3369
  });
3286
3370
 
3287
3371
  // src/lib/session-kill-telemetry.ts
3288
- import crypto2 from "crypto";
3372
+ import crypto from "crypto";
3289
3373
  async function recordSessionKill(input) {
3290
3374
  try {
3291
3375
  const client = getClient();
@@ -3295,7 +3379,7 @@ async function recordSessionKill(input) {
3295
3379
  ticks_idle, estimated_tokens_saved)
3296
3380
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
3297
3381
  args: [
3298
- crypto2.randomUUID(),
3382
+ crypto.randomUUID(),
3299
3383
  input.sessionName,
3300
3384
  input.agentId,
3301
3385
  (/* @__PURE__ */ new Date()).toISOString(),
@@ -3618,6 +3702,7 @@ __export(tmux_routing_exports, {
3618
3702
  isEmployeeAlive: () => isEmployeeAlive,
3619
3703
  isExeSession: () => isExeSession,
3620
3704
  isSessionBusy: () => isSessionBusy,
3705
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
3621
3706
  notifyParentExe: () => notifyParentExe,
3622
3707
  parseParentExe: () => parseParentExe,
3623
3708
  registerParentExe: () => registerParentExe,
@@ -3628,13 +3713,13 @@ __export(tmux_routing_exports, {
3628
3713
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
3629
3714
  });
3630
3715
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
3631
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync, readdirSync as readdirSync3 } from "fs";
3632
- import path12 from "path";
3716
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync, readdirSync as readdirSync2 } from "fs";
3717
+ import path11 from "path";
3633
3718
  import os8 from "os";
3634
3719
  import { fileURLToPath } from "url";
3635
- import { unlinkSync as unlinkSync3 } from "fs";
3720
+ import { unlinkSync as unlinkSync2 } from "fs";
3636
3721
  function spawnLockPath(sessionName) {
3637
- return path12.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3722
+ return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3638
3723
  }
3639
3724
  function isProcessAlive(pid) {
3640
3725
  try {
@@ -3651,7 +3736,7 @@ function acquireSpawnLock(sessionName) {
3651
3736
  const lockFile = spawnLockPath(sessionName);
3652
3737
  if (existsSync11(lockFile)) {
3653
3738
  try {
3654
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
3739
+ const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
3655
3740
  const age = Date.now() - lock.timestamp;
3656
3741
  if (isProcessAlive(lock.pid) && age < 6e4) {
3657
3742
  return false;
@@ -3664,15 +3749,15 @@ function acquireSpawnLock(sessionName) {
3664
3749
  }
3665
3750
  function releaseSpawnLock(sessionName) {
3666
3751
  try {
3667
- unlinkSync3(spawnLockPath(sessionName));
3752
+ unlinkSync2(spawnLockPath(sessionName));
3668
3753
  } catch {
3669
3754
  }
3670
3755
  }
3671
3756
  function resolveBehaviorsExporterScript() {
3672
3757
  try {
3673
3758
  const thisFile = fileURLToPath(import.meta.url);
3674
- const scriptPath = path12.join(
3675
- path12.dirname(thisFile),
3759
+ const scriptPath = path11.join(
3760
+ path11.dirname(thisFile),
3676
3761
  "..",
3677
3762
  "bin",
3678
3763
  "exe-export-behaviors.js"
@@ -3747,7 +3832,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3747
3832
  mkdirSync6(SESSION_CACHE, { recursive: true });
3748
3833
  }
3749
3834
  const rootExe = extractRootExe(parentExe) ?? parentExe;
3750
- const filePath = path12.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3835
+ const filePath = path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3751
3836
  writeFileSync6(filePath, JSON.stringify({
3752
3837
  parentExe: rootExe,
3753
3838
  dispatchedBy: dispatchedBy || rootExe,
@@ -3756,7 +3841,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3756
3841
  }
3757
3842
  function getParentExe(sessionKey) {
3758
3843
  try {
3759
- const data = JSON.parse(readFileSync9(path12.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3844
+ const data = JSON.parse(readFileSync8(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3760
3845
  return data.parentExe || null;
3761
3846
  } catch {
3762
3847
  return null;
@@ -3764,8 +3849,8 @@ function getParentExe(sessionKey) {
3764
3849
  }
3765
3850
  function getDispatchedBy(sessionKey) {
3766
3851
  try {
3767
- const data = JSON.parse(readFileSync9(
3768
- path12.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3852
+ const data = JSON.parse(readFileSync8(
3853
+ path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3769
3854
  "utf8"
3770
3855
  ));
3771
3856
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -3836,7 +3921,7 @@ async function verifyPaneAtCapacity(sessionName) {
3836
3921
  function readDebounceState() {
3837
3922
  try {
3838
3923
  if (!existsSync11(DEBOUNCE_FILE)) return {};
3839
- const raw = JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
3924
+ const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
3840
3925
  const state = {};
3841
3926
  for (const [key, val] of Object.entries(raw)) {
3842
3927
  if (typeof val === "number") {
@@ -3951,7 +4036,7 @@ function sendIntercom(targetSession) {
3951
4036
  try {
3952
4037
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
3953
4038
  const agent = baseAgentName(rawAgent);
3954
- const markerPath = path12.join(SESSION_CACHE, `current-task-${agent}.json`);
4039
+ const markerPath = path11.join(SESSION_CACHE, `current-task-${agent}.json`);
3955
4040
  if (existsSync11(markerPath)) {
3956
4041
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
3957
4042
  return "debounced";
@@ -3961,9 +4046,9 @@ function sendIntercom(targetSession) {
3961
4046
  try {
3962
4047
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
3963
4048
  const agent = baseAgentName(rawAgent);
3964
- const taskDir = path12.join(process.cwd(), "exe", agent);
4049
+ const taskDir = path11.join(process.cwd(), "exe", agent);
3965
4050
  if (existsSync11(taskDir)) {
3966
- const files = readdirSync3(taskDir).filter(
4051
+ const files = readdirSync2(taskDir).filter(
3967
4052
  (f) => f.endsWith(".md") && f !== "DONE.txt"
3968
4053
  );
3969
4054
  if (files.length === 0) {
@@ -4022,6 +4107,21 @@ function notifyParentExe(sessionKey) {
4022
4107
  }
4023
4108
  return true;
4024
4109
  }
4110
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
4111
+ const transport = getTransport();
4112
+ try {
4113
+ const sessions = transport.listSessions();
4114
+ if (!sessions.includes(coordinatorSession)) return false;
4115
+ execSync4(
4116
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
4117
+ { timeout: 3e3 }
4118
+ );
4119
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
4120
+ return true;
4121
+ } catch {
4122
+ return false;
4123
+ }
4124
+ }
4025
4125
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4026
4126
  if (isCoordinatorName(employeeName)) {
4027
4127
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -4095,8 +4195,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4095
4195
  const transport = getTransport();
4096
4196
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4097
4197
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4098
- const logDir = path12.join(os8.homedir(), ".exe-os", "session-logs");
4099
- const logFile = path12.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4198
+ const logDir = path11.join(os8.homedir(), ".exe-os", "session-logs");
4199
+ const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4100
4200
  if (!existsSync11(logDir)) {
4101
4201
  mkdirSync6(logDir, { recursive: true });
4102
4202
  }
@@ -4104,17 +4204,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4104
4204
  let cleanupSuffix = "";
4105
4205
  try {
4106
4206
  const thisFile = fileURLToPath(import.meta.url);
4107
- const cleanupScript = path12.join(path12.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4207
+ const cleanupScript = path11.join(path11.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4108
4208
  if (existsSync11(cleanupScript)) {
4109
4209
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
4110
4210
  }
4111
4211
  } catch {
4112
4212
  }
4113
4213
  try {
4114
- const claudeJsonPath = path12.join(os8.homedir(), ".claude.json");
4214
+ const claudeJsonPath = path11.join(os8.homedir(), ".claude.json");
4115
4215
  let claudeJson = {};
4116
4216
  try {
4117
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
4217
+ claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
4118
4218
  } catch {
4119
4219
  }
4120
4220
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -4126,13 +4226,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4126
4226
  } catch {
4127
4227
  }
4128
4228
  try {
4129
- const settingsDir = path12.join(os8.homedir(), ".claude", "projects");
4229
+ const settingsDir = path11.join(os8.homedir(), ".claude", "projects");
4130
4230
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4131
- const projSettingsDir = path12.join(settingsDir, normalizedKey);
4132
- const settingsPath = path12.join(projSettingsDir, "settings.json");
4231
+ const projSettingsDir = path11.join(settingsDir, normalizedKey);
4232
+ const settingsPath = path11.join(projSettingsDir, "settings.json");
4133
4233
  let settings = {};
4134
4234
  try {
4135
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
4235
+ settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
4136
4236
  } catch {
4137
4237
  }
4138
4238
  const perms = settings.permissions ?? {};
@@ -4176,7 +4276,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4176
4276
  let behaviorsFlag = "";
4177
4277
  let legacyFallbackWarned = false;
4178
4278
  if (!useExeAgent && !useBinSymlink) {
4179
- const identityPath = path12.join(
4279
+ const identityPath = path11.join(
4180
4280
  os8.homedir(),
4181
4281
  ".exe-os",
4182
4282
  "identity",
@@ -4192,7 +4292,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4192
4292
  }
4193
4293
  const behaviorsFile = exportBehaviorsSync(
4194
4294
  employeeName,
4195
- path12.basename(spawnCwd),
4295
+ path11.basename(spawnCwd),
4196
4296
  sessionName
4197
4297
  );
4198
4298
  if (behaviorsFile) {
@@ -4207,9 +4307,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4207
4307
  }
4208
4308
  let sessionContextFlag = "";
4209
4309
  try {
4210
- const ctxDir = path12.join(os8.homedir(), ".exe-os", "session-cache");
4310
+ const ctxDir = path11.join(os8.homedir(), ".exe-os", "session-cache");
4211
4311
  mkdirSync6(ctxDir, { recursive: true });
4212
- const ctxFile = path12.join(ctxDir, `session-context-${sessionName}.md`);
4312
+ const ctxFile = path11.join(ctxDir, `session-context-${sessionName}.md`);
4213
4313
  const ctxContent = [
4214
4314
  `## Session Context`,
4215
4315
  `You are running in tmux session: ${sessionName}.`,
@@ -4293,7 +4393,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4293
4393
  transport.pipeLog(sessionName, logFile);
4294
4394
  try {
4295
4395
  const mySession = getMySession();
4296
- const dispatchInfo = path12.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4396
+ const dispatchInfo = path11.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4297
4397
  writeFileSync6(dispatchInfo, JSON.stringify({
4298
4398
  dispatchedBy: mySession,
4299
4399
  rootExe: exeSession,
@@ -4368,15 +4468,15 @@ var init_tmux_routing = __esm({
4368
4468
  init_intercom_queue();
4369
4469
  init_plan_limits();
4370
4470
  init_employees();
4371
- SPAWN_LOCK_DIR = path12.join(os8.homedir(), ".exe-os", "spawn-locks");
4372
- SESSION_CACHE = path12.join(os8.homedir(), ".exe-os", "session-cache");
4471
+ SPAWN_LOCK_DIR = path11.join(os8.homedir(), ".exe-os", "spawn-locks");
4472
+ SESSION_CACHE = path11.join(os8.homedir(), ".exe-os", "session-cache");
4373
4473
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4374
4474
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
4375
4475
  VERIFY_PANE_LINES = 200;
4376
4476
  INTERCOM_DEBOUNCE_MS = 3e4;
4377
4477
  CODEX_DEBOUNCE_MS = 12e4;
4378
- INTERCOM_LOG2 = path12.join(os8.homedir(), ".exe-os", "intercom.log");
4379
- DEBOUNCE_FILE = path12.join(SESSION_CACHE, "intercom-debounce.json");
4478
+ INTERCOM_LOG2 = path11.join(os8.homedir(), ".exe-os", "intercom.log");
4479
+ DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
4380
4480
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4381
4481
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
4382
4482
  }
@@ -4399,6 +4499,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
4399
4499
  args: [scope]
4400
4500
  };
4401
4501
  }
4502
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
4503
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
4504
+ if (!scope) return { sql: "", args: [] };
4505
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
4506
+ return {
4507
+ sql: ` AND ${col} = ?`,
4508
+ args: [scope]
4509
+ };
4510
+ }
4402
4511
  var init_task_scope = __esm({
4403
4512
  "src/lib/task-scope.ts"() {
4404
4513
  "use strict";
@@ -4406,13 +4515,70 @@ var init_task_scope = __esm({
4406
4515
  }
4407
4516
  });
4408
4517
 
4518
+ // src/lib/notifications.ts
4519
+ import crypto2 from "crypto";
4520
+ import path12 from "path";
4521
+ import os9 from "os";
4522
+ import {
4523
+ readFileSync as readFileSync9,
4524
+ readdirSync as readdirSync3,
4525
+ unlinkSync as unlinkSync3,
4526
+ existsSync as existsSync12,
4527
+ rmdirSync
4528
+ } from "fs";
4529
+ async function writeNotification(notification) {
4530
+ try {
4531
+ const client = getClient();
4532
+ const id = crypto2.randomUUID();
4533
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4534
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
4535
+ await client.execute({
4536
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
4537
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
4538
+ args: [
4539
+ id,
4540
+ notification.agentId,
4541
+ notification.agentRole,
4542
+ notification.event,
4543
+ notification.project,
4544
+ notification.summary,
4545
+ notification.taskFile ?? null,
4546
+ sessionScope,
4547
+ now
4548
+ ]
4549
+ });
4550
+ } catch (err) {
4551
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
4552
+ `);
4553
+ }
4554
+ }
4555
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
4556
+ try {
4557
+ const client = getClient();
4558
+ const scope = strictSessionScopeFilter(sessionScope);
4559
+ await client.execute({
4560
+ sql: `UPDATE notifications SET read = 1
4561
+ WHERE task_file = ? AND read = 0${scope.sql}`,
4562
+ args: [taskFile, ...scope.args]
4563
+ });
4564
+ } catch {
4565
+ }
4566
+ }
4567
+ var init_notifications = __esm({
4568
+ "src/lib/notifications.ts"() {
4569
+ "use strict";
4570
+ init_database();
4571
+ init_task_scope();
4572
+ }
4573
+ });
4574
+
4409
4575
  // src/lib/tasks-crud.ts
4410
4576
  import crypto3 from "crypto";
4411
4577
  import path13 from "path";
4412
- import os9 from "os";
4578
+ import os10 from "os";
4413
4579
  import { execSync as execSync5 } from "child_process";
4414
4580
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
4415
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
4581
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
4416
4582
  async function writeCheckpoint(input) {
4417
4583
  const client = getClient();
4418
4584
  const row = await resolveTask(client, input.taskId);
@@ -4624,13 +4790,19 @@ ${laneWarning}` : laneWarning;
4624
4790
  });
4625
4791
  if (input.baseDir) {
4626
4792
  try {
4627
- const EXE_OS_DIR = path13.join(os9.homedir(), ".exe-os");
4793
+ const EXE_OS_DIR = path13.join(os10.homedir(), ".exe-os");
4628
4794
  const mdPath = path13.join(EXE_OS_DIR, taskFile);
4629
4795
  const mdDir = path13.dirname(mdPath);
4630
- if (!existsSync12(mdDir)) await mkdir4(mdDir, { recursive: true });
4796
+ if (!existsSync13(mdDir)) await mkdir4(mdDir, { recursive: true });
4631
4797
  const reviewer = input.reviewer ?? input.assignedBy;
4632
4798
  const mdContent = `# ${input.title}
4633
4799
 
4800
+ ## MANDATORY: When done
4801
+
4802
+ You MUST call update_task with status "done" and a result summary when finished.
4803
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
4804
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
4805
+
4634
4806
  **ID:** ${id}
4635
4807
  **Status:** ${initialStatus}
4636
4808
  **Priority:** ${input.priority}
@@ -4644,12 +4816,6 @@ ${laneWarning}` : laneWarning;
4644
4816
  ## Context
4645
4817
 
4646
4818
  ${input.context}
4647
-
4648
- ## MANDATORY: When done
4649
-
4650
- You MUST call update_task with status "done" and a result summary when finished.
4651
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
4652
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
4653
4819
  `;
4654
4820
  await writeFile4(mdPath, mdContent, "utf-8");
4655
4821
  } catch (err) {
@@ -4898,7 +5064,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
4898
5064
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
4899
5065
  } catch {
4900
5066
  }
4901
- if (input.status === "done" || input.status === "cancelled") {
5067
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
4902
5068
  try {
4903
5069
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
4904
5070
  clearQueueForAgent2(String(row.assigned_to));
@@ -4929,7 +5095,7 @@ async function deleteTaskCore(taskId, _baseDir) {
4929
5095
  async function ensureArchitectureDoc(baseDir, projectName) {
4930
5096
  const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
4931
5097
  try {
4932
- if (existsSync12(archPath)) return;
5098
+ if (existsSync13(archPath)) return;
4933
5099
  const template = [
4934
5100
  `# ${projectName} \u2014 System Architecture`,
4935
5101
  "",
@@ -4964,7 +5130,7 @@ async function ensureArchitectureDoc(baseDir, projectName) {
4964
5130
  async function ensureGitignoreExe(baseDir) {
4965
5131
  const gitignorePath = path13.join(baseDir, ".gitignore");
4966
5132
  try {
4967
- if (existsSync12(gitignorePath)) {
5133
+ if (existsSync13(gitignorePath)) {
4968
5134
  const content = readFileSync10(gitignorePath, "utf-8");
4969
5135
  if (/^\/?exe\/?$/m.test(content)) return;
4970
5136
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -4997,57 +5163,41 @@ var init_tasks_crud = __esm({
4997
5163
 
4998
5164
  // src/lib/tasks-review.ts
4999
5165
  import path14 from "path";
5000
- import { existsSync as existsSync13, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
5166
+ import { existsSync as existsSync14, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
5001
5167
  async function countPendingReviews(sessionScope) {
5002
5168
  const client = getClient();
5003
- if (sessionScope) {
5004
- const result2 = await client.execute({
5005
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND session_scope = ?",
5006
- args: [sessionScope]
5007
- });
5008
- return Number(result2.rows[0]?.cnt) || 0;
5009
- }
5169
+ const scope = strictSessionScopeFilter(
5170
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5171
+ );
5010
5172
  const result = await client.execute({
5011
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
5012
- args: []
5173
+ sql: `SELECT COUNT(*) as cnt FROM tasks
5174
+ WHERE status = 'needs_review'${scope.sql}`,
5175
+ args: [...scope.args]
5013
5176
  });
5014
5177
  return Number(result.rows[0]?.cnt) || 0;
5015
5178
  }
5016
5179
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
5017
5180
  const client = getClient();
5018
- if (sessionScope) {
5019
- const result2 = await client.execute({
5020
- sql: `SELECT COUNT(*) as cnt FROM tasks
5021
- WHERE status = 'needs_review' AND updated_at > ?
5022
- AND session_scope = ?`,
5023
- args: [sinceIso, sessionScope]
5024
- });
5025
- return Number(result2.rows[0]?.cnt) || 0;
5026
- }
5181
+ const scope = strictSessionScopeFilter(
5182
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5183
+ );
5027
5184
  const result = await client.execute({
5028
5185
  sql: `SELECT COUNT(*) as cnt FROM tasks
5029
- WHERE status = 'needs_review' AND updated_at > ?`,
5030
- args: [sinceIso]
5186
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
5187
+ args: [sinceIso, ...scope.args]
5031
5188
  });
5032
5189
  return Number(result.rows[0]?.cnt) || 0;
5033
5190
  }
5034
5191
  async function listPendingReviews(limit, sessionScope) {
5035
5192
  const client = getClient();
5036
- if (sessionScope) {
5037
- const result2 = await client.execute({
5038
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
5039
- WHERE status = 'needs_review'
5040
- AND session_scope = ?
5041
- ORDER BY updated_at ASC LIMIT ?`,
5042
- args: [sessionScope, limit]
5043
- });
5044
- return result2.rows;
5045
- }
5193
+ const scope = strictSessionScopeFilter(
5194
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5195
+ );
5046
5196
  const result = await client.execute({
5047
5197
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
5048
- WHERE status = 'needs_review'
5198
+ WHERE status = 'needs_review'${scope.sql}
5049
5199
  ORDER BY updated_at ASC LIMIT ?`,
5050
- args: [limit]
5200
+ args: [...scope.args, limit]
5051
5201
  });
5052
5202
  return result.rows;
5053
5203
  }
@@ -5059,7 +5209,7 @@ async function cleanupOrphanedReviews() {
5059
5209
  WHERE status IN ('open', 'needs_review', 'in_progress')
5060
5210
  AND assigned_by = 'system'
5061
5211
  AND title LIKE 'Review:%'
5062
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
5212
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
5063
5213
  args: [now]
5064
5214
  });
5065
5215
  const r1b = await client.execute({
@@ -5179,7 +5329,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
5179
5329
  }
5180
5330
  try {
5181
5331
  const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
5182
- if (existsSync13(cacheDir)) {
5332
+ if (existsSync14(cacheDir)) {
5183
5333
  for (const f of readdirSync4(cacheDir)) {
5184
5334
  if (f.startsWith("review-notified-")) {
5185
5335
  unlinkSync4(path14.join(cacheDir, f));
@@ -5199,6 +5349,7 @@ var init_tasks_review = __esm({
5199
5349
  init_tmux_routing();
5200
5350
  init_session_key();
5201
5351
  init_state_bus();
5352
+ init_task_scope();
5202
5353
  }
5203
5354
  });
5204
5355
 
@@ -5255,7 +5406,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
5255
5406
  const scScope = sessionScopeFilter();
5256
5407
  const remaining = await client.execute({
5257
5408
  sql: `SELECT COUNT(*) as cnt FROM tasks
5258
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
5409
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
5259
5410
  args: [parentTaskId, ...scScope.args]
5260
5411
  });
5261
5412
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -5813,7 +5964,7 @@ async function updateTask(input) {
5813
5964
  if (input.status === "in_progress") {
5814
5965
  mkdirSync7(cacheDir, { recursive: true });
5815
5966
  writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
5816
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
5967
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
5817
5968
  try {
5818
5969
  unlinkSync5(cachePath);
5819
5970
  } catch {
@@ -5821,10 +5972,10 @@ async function updateTask(input) {
5821
5972
  }
5822
5973
  } catch {
5823
5974
  }
5824
- if (input.status === "done") {
5975
+ if (input.status === "done" || input.status === "closed") {
5825
5976
  await cleanupReviewFile(row, taskFile, input.baseDir);
5826
5977
  }
5827
- if (input.status === "done" || input.status === "cancelled") {
5978
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
5828
5979
  try {
5829
5980
  const client = getClient();
5830
5981
  const taskTitle = String(row.title);
@@ -5840,7 +5991,7 @@ async function updateTask(input) {
5840
5991
  if (!isCoordinatorName(assignedAgent)) {
5841
5992
  try {
5842
5993
  const draftClient = getClient();
5843
- if (input.status === "done") {
5994
+ if (input.status === "done" || input.status === "closed") {
5844
5995
  await draftClient.execute({
5845
5996
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
5846
5997
  args: [assignedAgent]
@@ -5857,7 +6008,7 @@ async function updateTask(input) {
5857
6008
  try {
5858
6009
  const client = getClient();
5859
6010
  const cascaded = await client.execute({
5860
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
6011
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
5861
6012
  WHERE parent_task_id = ? AND status = 'needs_review'`,
5862
6013
  args: [now, taskId]
5863
6014
  });
@@ -5870,14 +6021,14 @@ async function updateTask(input) {
5870
6021
  } catch {
5871
6022
  }
5872
6023
  }
5873
- const isTerminal = input.status === "done" || input.status === "needs_review";
6024
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
5874
6025
  if (isTerminal) {
5875
6026
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
5876
6027
  if (!isCoordinator) {
5877
6028
  notifyTaskDone();
5878
6029
  }
5879
6030
  await markTaskNotificationsRead(taskFile);
5880
- if (input.status === "done") {
6031
+ if (input.status === "done" || input.status === "closed") {
5881
6032
  try {
5882
6033
  await cascadeUnblock(taskId, input.baseDir, now);
5883
6034
  } catch {
@@ -5897,7 +6048,7 @@ async function updateTask(input) {
5897
6048
  }
5898
6049
  }
5899
6050
  }
5900
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
6051
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
5901
6052
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
5902
6053
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
5903
6054
  taskId,
@@ -5975,7 +6126,7 @@ init_database();
5975
6126
 
5976
6127
  // src/lib/keychain.ts
5977
6128
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
5978
- import { existsSync as existsSync4 } from "fs";
6129
+ import { existsSync as existsSync5 } from "fs";
5979
6130
  import path5 from "path";
5980
6131
  import os4 from "os";
5981
6132
  var SERVICE = "exe-mem";
@@ -6005,7 +6156,7 @@ async function getMasterKey() {
6005
6156
  }
6006
6157
  }
6007
6158
  const keyPath = getKeyPath();
6008
- if (!existsSync4(keyPath)) {
6159
+ if (!existsSync5(keyPath)) {
6009
6160
  process.stderr.write(
6010
6161
  `[keychain] Key not found at ${keyPath} (HOME=${os4.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
6011
6162
  `