@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
@@ -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,174 @@ var init_task_scope = __esm({
4406
4515
  }
4407
4516
  });
4408
4517
 
4409
- // src/lib/tasks-crud.ts
4410
- import crypto3 from "crypto";
4411
- import path13 from "path";
4518
+ // src/lib/notifications.ts
4519
+ import crypto2 from "crypto";
4520
+ import path12 from "path";
4412
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
+
4575
+ // src/lib/project-name.ts
4413
4576
  import { execSync as execSync5 } from "child_process";
4577
+ import path13 from "path";
4578
+ function getProjectName(cwd) {
4579
+ const dir = cwd ?? process.cwd();
4580
+ if (_cached2 && _cachedCwd === dir) return _cached2;
4581
+ try {
4582
+ let repoRoot;
4583
+ try {
4584
+ const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
4585
+ cwd: dir,
4586
+ encoding: "utf8",
4587
+ timeout: 2e3,
4588
+ stdio: ["pipe", "pipe", "pipe"]
4589
+ }).trim();
4590
+ repoRoot = path13.dirname(gitCommonDir);
4591
+ } catch {
4592
+ repoRoot = execSync5("git rev-parse --show-toplevel", {
4593
+ cwd: dir,
4594
+ encoding: "utf8",
4595
+ timeout: 2e3,
4596
+ stdio: ["pipe", "pipe", "pipe"]
4597
+ }).trim();
4598
+ }
4599
+ _cached2 = path13.basename(repoRoot);
4600
+ _cachedCwd = dir;
4601
+ return _cached2;
4602
+ } catch {
4603
+ _cached2 = path13.basename(dir);
4604
+ _cachedCwd = dir;
4605
+ return _cached2;
4606
+ }
4607
+ }
4608
+ var _cached2, _cachedCwd;
4609
+ var init_project_name = __esm({
4610
+ "src/lib/project-name.ts"() {
4611
+ "use strict";
4612
+ _cached2 = null;
4613
+ _cachedCwd = null;
4614
+ }
4615
+ });
4616
+
4617
+ // src/lib/session-scope.ts
4618
+ var session_scope_exports = {};
4619
+ __export(session_scope_exports, {
4620
+ assertSessionScope: () => assertSessionScope,
4621
+ findSessionForProject: () => findSessionForProject,
4622
+ getSessionProject: () => getSessionProject
4623
+ });
4624
+ function getSessionProject(sessionName) {
4625
+ const sessions = listSessions();
4626
+ const entry = sessions.find((s) => s.windowName === sessionName);
4627
+ if (!entry) return null;
4628
+ const parts = entry.projectDir.split("/").filter(Boolean);
4629
+ return parts[parts.length - 1] ?? null;
4630
+ }
4631
+ function findSessionForProject(projectName) {
4632
+ const sessions = listSessions();
4633
+ for (const s of sessions) {
4634
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
4635
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
4636
+ }
4637
+ return null;
4638
+ }
4639
+ function assertSessionScope(actionType, targetProject) {
4640
+ try {
4641
+ const currentProject = getProjectName();
4642
+ const exeSession = resolveExeSession();
4643
+ if (!exeSession) {
4644
+ return { allowed: true, reason: "no_session" };
4645
+ }
4646
+ if (currentProject === targetProject) {
4647
+ return {
4648
+ allowed: true,
4649
+ reason: "same_session",
4650
+ currentProject,
4651
+ targetProject
4652
+ };
4653
+ }
4654
+ process.stderr.write(
4655
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
4656
+ `
4657
+ );
4658
+ return {
4659
+ allowed: false,
4660
+ reason: "cross_session_denied",
4661
+ currentProject,
4662
+ targetProject,
4663
+ targetSession: findSessionForProject(targetProject)?.windowName
4664
+ };
4665
+ } catch {
4666
+ return { allowed: true, reason: "no_session" };
4667
+ }
4668
+ }
4669
+ var init_session_scope = __esm({
4670
+ "src/lib/session-scope.ts"() {
4671
+ "use strict";
4672
+ init_session_registry();
4673
+ init_project_name();
4674
+ init_tmux_routing();
4675
+ init_employees();
4676
+ }
4677
+ });
4678
+
4679
+ // src/lib/tasks-crud.ts
4680
+ import crypto3 from "crypto";
4681
+ import path14 from "path";
4682
+ import os10 from "os";
4683
+ import { execSync as execSync6 } from "child_process";
4414
4684
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
4415
- import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
4685
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
4416
4686
  async function writeCheckpoint(input) {
4417
4687
  const client = getClient();
4418
4688
  const row = await resolveTask(client, input.taskId);
@@ -4532,9 +4802,24 @@ async function createTaskCore(input) {
4532
4802
  const now = (/* @__PURE__ */ new Date()).toISOString();
4533
4803
  const slug = slugify(input.title);
4534
4804
  let earlySessionScope = null;
4805
+ let scopeMismatchWarning;
4535
4806
  try {
4536
4807
  const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
4537
- earlySessionScope = resolveExeSession2();
4808
+ const resolved = resolveExeSession2();
4809
+ if (resolved && input.projectName) {
4810
+ const { getSessionProject: getSessionProject2 } = await Promise.resolve().then(() => (init_session_scope(), session_scope_exports));
4811
+ const sessionProject = getSessionProject2(resolved);
4812
+ if (sessionProject && sessionProject !== input.projectName) {
4813
+ scopeMismatchWarning = `session/project mismatch: session "${resolved}" owns "${sessionProject}" but task targets "${input.projectName}". Routed to default scope.`;
4814
+ process.stderr.write(`[create_task] ${scopeMismatchWarning}
4815
+ `);
4816
+ earlySessionScope = null;
4817
+ } else {
4818
+ earlySessionScope = resolved;
4819
+ }
4820
+ } else {
4821
+ earlySessionScope = resolved;
4822
+ }
4538
4823
  } catch {
4539
4824
  }
4540
4825
  const scope = earlySessionScope ?? "default";
@@ -4585,10 +4870,14 @@ async function createTaskCore(input) {
4585
4870
  ${laneWarning}` : laneWarning;
4586
4871
  }
4587
4872
  }
4873
+ if (scopeMismatchWarning) {
4874
+ warning = warning ? `${warning}
4875
+ ${scopeMismatchWarning}` : scopeMismatchWarning;
4876
+ }
4588
4877
  if (input.baseDir) {
4589
4878
  try {
4590
- await mkdir4(path13.join(input.baseDir, "exe", "output"), { recursive: true });
4591
- await mkdir4(path13.join(input.baseDir, "exe", "research"), { recursive: true });
4879
+ await mkdir4(path14.join(input.baseDir, "exe", "output"), { recursive: true });
4880
+ await mkdir4(path14.join(input.baseDir, "exe", "research"), { recursive: true });
4592
4881
  await ensureArchitectureDoc(input.baseDir, input.projectName);
4593
4882
  await ensureGitignoreExe(input.baseDir);
4594
4883
  } catch {
@@ -4624,13 +4913,19 @@ ${laneWarning}` : laneWarning;
4624
4913
  });
4625
4914
  if (input.baseDir) {
4626
4915
  try {
4627
- const EXE_OS_DIR = path13.join(os9.homedir(), ".exe-os");
4628
- const mdPath = path13.join(EXE_OS_DIR, taskFile);
4629
- const mdDir = path13.dirname(mdPath);
4630
- if (!existsSync12(mdDir)) await mkdir4(mdDir, { recursive: true });
4916
+ const EXE_OS_DIR = path14.join(os10.homedir(), ".exe-os");
4917
+ const mdPath = path14.join(EXE_OS_DIR, taskFile);
4918
+ const mdDir = path14.dirname(mdPath);
4919
+ if (!existsSync13(mdDir)) await mkdir4(mdDir, { recursive: true });
4631
4920
  const reviewer = input.reviewer ?? input.assignedBy;
4632
4921
  const mdContent = `# ${input.title}
4633
4922
 
4923
+ ## MANDATORY: When done
4924
+
4925
+ You MUST call update_task with status "done" and a result summary when finished.
4926
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
4927
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
4928
+
4634
4929
  **ID:** ${id}
4635
4930
  **Status:** ${initialStatus}
4636
4931
  **Priority:** ${input.priority}
@@ -4644,12 +4939,6 @@ ${laneWarning}` : laneWarning;
4644
4939
  ## Context
4645
4940
 
4646
4941
  ${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
4942
  `;
4654
4943
  await writeFile4(mdPath, mdContent, "utf-8");
4655
4944
  } catch (err) {
@@ -4731,14 +5020,14 @@ function isTmuxSessionAlive(identifier) {
4731
5020
  if (!identifier || identifier === "unknown") return true;
4732
5021
  try {
4733
5022
  if (identifier.startsWith("%")) {
4734
- const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
5023
+ const output = execSync6("tmux list-panes -a -F '#{pane_id}'", {
4735
5024
  timeout: 2e3,
4736
5025
  encoding: "utf8",
4737
5026
  stdio: ["pipe", "pipe", "pipe"]
4738
5027
  });
4739
5028
  return output.split("\n").some((l) => l.trim() === identifier);
4740
5029
  } else {
4741
- execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
5030
+ execSync6(`tmux has-session -t ${JSON.stringify(identifier)}`, {
4742
5031
  timeout: 2e3,
4743
5032
  stdio: ["pipe", "pipe", "pipe"]
4744
5033
  });
@@ -4747,7 +5036,7 @@ function isTmuxSessionAlive(identifier) {
4747
5036
  } catch {
4748
5037
  if (identifier.startsWith("%")) return true;
4749
5038
  try {
4750
- execSync5("tmux list-sessions", {
5039
+ execSync6("tmux list-sessions", {
4751
5040
  timeout: 2e3,
4752
5041
  stdio: ["pipe", "pipe", "pipe"]
4753
5042
  });
@@ -4762,12 +5051,12 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
4762
5051
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
4763
5052
  try {
4764
5053
  const since = new Date(taskCreatedAt).toISOString();
4765
- const branch = execSync5(
5054
+ const branch = execSync6(
4766
5055
  "git rev-parse --abbrev-ref HEAD 2>/dev/null",
4767
5056
  { encoding: "utf8", timeout: 3e3 }
4768
5057
  ).trim();
4769
5058
  const branchArg = branch && branch !== "HEAD" ? branch : "";
4770
- const commitCount = execSync5(
5059
+ const commitCount = execSync6(
4771
5060
  `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
4772
5061
  { encoding: "utf8", timeout: 5e3 }
4773
5062
  ).trim();
@@ -4898,7 +5187,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
4898
5187
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
4899
5188
  } catch {
4900
5189
  }
4901
- if (input.status === "done" || input.status === "cancelled") {
5190
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
4902
5191
  try {
4903
5192
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
4904
5193
  clearQueueForAgent2(String(row.assigned_to));
@@ -4927,9 +5216,9 @@ async function deleteTaskCore(taskId, _baseDir) {
4927
5216
  return { taskFile, assignedTo, assignedBy, taskSlug };
4928
5217
  }
4929
5218
  async function ensureArchitectureDoc(baseDir, projectName) {
4930
- const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
5219
+ const archPath = path14.join(baseDir, "exe", "ARCHITECTURE.md");
4931
5220
  try {
4932
- if (existsSync12(archPath)) return;
5221
+ if (existsSync13(archPath)) return;
4933
5222
  const template = [
4934
5223
  `# ${projectName} \u2014 System Architecture`,
4935
5224
  "",
@@ -4962,9 +5251,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
4962
5251
  }
4963
5252
  }
4964
5253
  async function ensureGitignoreExe(baseDir) {
4965
- const gitignorePath = path13.join(baseDir, ".gitignore");
5254
+ const gitignorePath = path14.join(baseDir, ".gitignore");
4966
5255
  try {
4967
- if (existsSync12(gitignorePath)) {
5256
+ if (existsSync13(gitignorePath)) {
4968
5257
  const content = readFileSync10(gitignorePath, "utf-8");
4969
5258
  if (/^\/?exe\/?$/m.test(content)) return;
4970
5259
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -4996,58 +5285,42 @@ var init_tasks_crud = __esm({
4996
5285
  });
4997
5286
 
4998
5287
  // src/lib/tasks-review.ts
4999
- import path14 from "path";
5000
- import { existsSync as existsSync13, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
5288
+ import path15 from "path";
5289
+ import { existsSync as existsSync14, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
5001
5290
  async function countPendingReviews(sessionScope) {
5002
5291
  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
- }
5292
+ const scope = strictSessionScopeFilter(
5293
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5294
+ );
5010
5295
  const result = await client.execute({
5011
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
5012
- args: []
5296
+ sql: `SELECT COUNT(*) as cnt FROM tasks
5297
+ WHERE status = 'needs_review'${scope.sql}`,
5298
+ args: [...scope.args]
5013
5299
  });
5014
5300
  return Number(result.rows[0]?.cnt) || 0;
5015
5301
  }
5016
5302
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
5017
5303
  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
- }
5304
+ const scope = strictSessionScopeFilter(
5305
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5306
+ );
5027
5307
  const result = await client.execute({
5028
5308
  sql: `SELECT COUNT(*) as cnt FROM tasks
5029
- WHERE status = 'needs_review' AND updated_at > ?`,
5030
- args: [sinceIso]
5309
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
5310
+ args: [sinceIso, ...scope.args]
5031
5311
  });
5032
5312
  return Number(result.rows[0]?.cnt) || 0;
5033
5313
  }
5034
5314
  async function listPendingReviews(limit, sessionScope) {
5035
5315
  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
- }
5316
+ const scope = strictSessionScopeFilter(
5317
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5318
+ );
5046
5319
  const result = await client.execute({
5047
5320
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
5048
- WHERE status = 'needs_review'
5321
+ WHERE status = 'needs_review'${scope.sql}
5049
5322
  ORDER BY updated_at ASC LIMIT ?`,
5050
- args: [limit]
5323
+ args: [...scope.args, limit]
5051
5324
  });
5052
5325
  return result.rows;
5053
5326
  }
@@ -5059,7 +5332,7 @@ async function cleanupOrphanedReviews() {
5059
5332
  WHERE status IN ('open', 'needs_review', 'in_progress')
5060
5333
  AND assigned_by = 'system'
5061
5334
  AND title LIKE 'Review:%'
5062
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
5335
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
5063
5336
  args: [now]
5064
5337
  });
5065
5338
  const r1b = await client.execute({
@@ -5178,11 +5451,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
5178
5451
  );
5179
5452
  }
5180
5453
  try {
5181
- const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
5182
- if (existsSync13(cacheDir)) {
5454
+ const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
5455
+ if (existsSync14(cacheDir)) {
5183
5456
  for (const f of readdirSync4(cacheDir)) {
5184
5457
  if (f.startsWith("review-notified-")) {
5185
- unlinkSync4(path14.join(cacheDir, f));
5458
+ unlinkSync4(path15.join(cacheDir, f));
5186
5459
  }
5187
5460
  }
5188
5461
  }
@@ -5199,11 +5472,12 @@ var init_tasks_review = __esm({
5199
5472
  init_tmux_routing();
5200
5473
  init_session_key();
5201
5474
  init_state_bus();
5475
+ init_task_scope();
5202
5476
  }
5203
5477
  });
5204
5478
 
5205
5479
  // src/lib/tasks-chain.ts
5206
- import path15 from "path";
5480
+ import path16 from "path";
5207
5481
  import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
5208
5482
  async function cascadeUnblock(taskId, baseDir, now) {
5209
5483
  const client = getClient();
@@ -5220,7 +5494,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
5220
5494
  });
5221
5495
  for (const ur of unblockedRows.rows) {
5222
5496
  try {
5223
- const ubFile = path15.join(baseDir, String(ur.task_file));
5497
+ const ubFile = path16.join(baseDir, String(ur.task_file));
5224
5498
  let ubContent = await readFile4(ubFile, "utf-8");
5225
5499
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
5226
5500
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -5255,7 +5529,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
5255
5529
  const scScope = sessionScopeFilter();
5256
5530
  const remaining = await client.execute({
5257
5531
  sql: `SELECT COUNT(*) as cnt FROM tasks
5258
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
5532
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
5259
5533
  args: [parentTaskId, ...scScope.args]
5260
5534
  });
5261
5535
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -5287,110 +5561,6 @@ var init_tasks_chain = __esm({
5287
5561
  }
5288
5562
  });
5289
5563
 
5290
- // src/lib/project-name.ts
5291
- import { execSync as execSync6 } from "child_process";
5292
- import path16 from "path";
5293
- function getProjectName(cwd) {
5294
- const dir = cwd ?? process.cwd();
5295
- if (_cached2 && _cachedCwd === dir) return _cached2;
5296
- try {
5297
- let repoRoot;
5298
- try {
5299
- const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
5300
- cwd: dir,
5301
- encoding: "utf8",
5302
- timeout: 2e3,
5303
- stdio: ["pipe", "pipe", "pipe"]
5304
- }).trim();
5305
- repoRoot = path16.dirname(gitCommonDir);
5306
- } catch {
5307
- repoRoot = execSync6("git rev-parse --show-toplevel", {
5308
- cwd: dir,
5309
- encoding: "utf8",
5310
- timeout: 2e3,
5311
- stdio: ["pipe", "pipe", "pipe"]
5312
- }).trim();
5313
- }
5314
- _cached2 = path16.basename(repoRoot);
5315
- _cachedCwd = dir;
5316
- return _cached2;
5317
- } catch {
5318
- _cached2 = path16.basename(dir);
5319
- _cachedCwd = dir;
5320
- return _cached2;
5321
- }
5322
- }
5323
- var _cached2, _cachedCwd;
5324
- var init_project_name = __esm({
5325
- "src/lib/project-name.ts"() {
5326
- "use strict";
5327
- _cached2 = null;
5328
- _cachedCwd = null;
5329
- }
5330
- });
5331
-
5332
- // src/lib/session-scope.ts
5333
- var session_scope_exports = {};
5334
- __export(session_scope_exports, {
5335
- assertSessionScope: () => assertSessionScope,
5336
- findSessionForProject: () => findSessionForProject,
5337
- getSessionProject: () => getSessionProject
5338
- });
5339
- function getSessionProject(sessionName) {
5340
- const sessions = listSessions();
5341
- const entry = sessions.find((s) => s.windowName === sessionName);
5342
- if (!entry) return null;
5343
- const parts = entry.projectDir.split("/").filter(Boolean);
5344
- return parts[parts.length - 1] ?? null;
5345
- }
5346
- function findSessionForProject(projectName) {
5347
- const sessions = listSessions();
5348
- for (const s of sessions) {
5349
- const proj = s.projectDir.split("/").filter(Boolean).pop();
5350
- if (proj === projectName && isCoordinatorName(s.agentId)) return s;
5351
- }
5352
- return null;
5353
- }
5354
- function assertSessionScope(actionType, targetProject) {
5355
- try {
5356
- const currentProject = getProjectName();
5357
- const exeSession = resolveExeSession();
5358
- if (!exeSession) {
5359
- return { allowed: true, reason: "no_session" };
5360
- }
5361
- if (currentProject === targetProject) {
5362
- return {
5363
- allowed: true,
5364
- reason: "same_session",
5365
- currentProject,
5366
- targetProject
5367
- };
5368
- }
5369
- process.stderr.write(
5370
- `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
5371
- `
5372
- );
5373
- return {
5374
- allowed: false,
5375
- reason: "cross_session_denied",
5376
- currentProject,
5377
- targetProject,
5378
- targetSession: findSessionForProject(targetProject)?.windowName
5379
- };
5380
- } catch {
5381
- return { allowed: true, reason: "no_session" };
5382
- }
5383
- }
5384
- var init_session_scope = __esm({
5385
- "src/lib/session-scope.ts"() {
5386
- "use strict";
5387
- init_session_registry();
5388
- init_project_name();
5389
- init_tmux_routing();
5390
- init_employees();
5391
- }
5392
- });
5393
-
5394
5564
  // src/lib/tasks-notify.ts
5395
5565
  async function dispatchTaskToEmployee(input) {
5396
5566
  if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
@@ -5813,7 +5983,7 @@ async function updateTask(input) {
5813
5983
  if (input.status === "in_progress") {
5814
5984
  mkdirSync7(cacheDir, { recursive: true });
5815
5985
  writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
5816
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
5986
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
5817
5987
  try {
5818
5988
  unlinkSync5(cachePath);
5819
5989
  } catch {
@@ -5821,10 +5991,10 @@ async function updateTask(input) {
5821
5991
  }
5822
5992
  } catch {
5823
5993
  }
5824
- if (input.status === "done") {
5994
+ if (input.status === "done" || input.status === "closed") {
5825
5995
  await cleanupReviewFile(row, taskFile, input.baseDir);
5826
5996
  }
5827
- if (input.status === "done" || input.status === "cancelled") {
5997
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
5828
5998
  try {
5829
5999
  const client = getClient();
5830
6000
  const taskTitle = String(row.title);
@@ -5840,7 +6010,7 @@ async function updateTask(input) {
5840
6010
  if (!isCoordinatorName(assignedAgent)) {
5841
6011
  try {
5842
6012
  const draftClient = getClient();
5843
- if (input.status === "done") {
6013
+ if (input.status === "done" || input.status === "closed") {
5844
6014
  await draftClient.execute({
5845
6015
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
5846
6016
  args: [assignedAgent]
@@ -5857,7 +6027,7 @@ async function updateTask(input) {
5857
6027
  try {
5858
6028
  const client = getClient();
5859
6029
  const cascaded = await client.execute({
5860
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
6030
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
5861
6031
  WHERE parent_task_id = ? AND status = 'needs_review'`,
5862
6032
  args: [now, taskId]
5863
6033
  });
@@ -5870,14 +6040,14 @@ async function updateTask(input) {
5870
6040
  } catch {
5871
6041
  }
5872
6042
  }
5873
- const isTerminal = input.status === "done" || input.status === "needs_review";
6043
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
5874
6044
  if (isTerminal) {
5875
6045
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
5876
6046
  if (!isCoordinator) {
5877
6047
  notifyTaskDone();
5878
6048
  }
5879
6049
  await markTaskNotificationsRead(taskFile);
5880
- if (input.status === "done") {
6050
+ if (input.status === "done" || input.status === "closed") {
5881
6051
  try {
5882
6052
  await cascadeUnblock(taskId, input.baseDir, now);
5883
6053
  } catch {
@@ -5897,7 +6067,7 @@ async function updateTask(input) {
5897
6067
  }
5898
6068
  }
5899
6069
  }
5900
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
6070
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
5901
6071
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
5902
6072
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
5903
6073
  taskId,
@@ -5975,7 +6145,7 @@ init_database();
5975
6145
 
5976
6146
  // src/lib/keychain.ts
5977
6147
  import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod as chmod2 } from "fs/promises";
5978
- import { existsSync as existsSync4 } from "fs";
6148
+ import { existsSync as existsSync5 } from "fs";
5979
6149
  import path5 from "path";
5980
6150
  import os4 from "os";
5981
6151
  var SERVICE = "exe-mem";
@@ -6005,7 +6175,7 @@ async function getMasterKey() {
6005
6175
  }
6006
6176
  }
6007
6177
  const keyPath = getKeyPath();
6008
- if (!existsSync4(keyPath)) {
6178
+ if (!existsSync5(keyPath)) {
6009
6179
  process.stderr.write(
6010
6180
  `[keychain] Key not found at ${keyPath} (HOME=${os4.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
6011
6181
  `