@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
@@ -32,6 +32,44 @@ var init_db_retry = __esm({
32
32
  }
33
33
  });
34
34
 
35
+ // src/lib/secure-files.ts
36
+ import { chmodSync, existsSync, mkdirSync } from "fs";
37
+ import { chmod, mkdir } from "fs/promises";
38
+ async function ensurePrivateDir(dirPath) {
39
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
40
+ try {
41
+ await chmod(dirPath, PRIVATE_DIR_MODE);
42
+ } catch {
43
+ }
44
+ }
45
+ function ensurePrivateDirSync(dirPath) {
46
+ mkdirSync(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
47
+ try {
48
+ chmodSync(dirPath, PRIVATE_DIR_MODE);
49
+ } catch {
50
+ }
51
+ }
52
+ async function enforcePrivateFile(filePath) {
53
+ try {
54
+ await chmod(filePath, PRIVATE_FILE_MODE);
55
+ } catch {
56
+ }
57
+ }
58
+ function enforcePrivateFileSync(filePath) {
59
+ try {
60
+ if (existsSync(filePath)) chmodSync(filePath, PRIVATE_FILE_MODE);
61
+ } catch {
62
+ }
63
+ }
64
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
65
+ var init_secure_files = __esm({
66
+ "src/lib/secure-files.ts"() {
67
+ "use strict";
68
+ PRIVATE_DIR_MODE = 448;
69
+ PRIVATE_FILE_MODE = 384;
70
+ }
71
+ });
72
+
35
73
  // src/lib/config.ts
36
74
  var config_exports = {};
37
75
  __export(config_exports, {
@@ -48,8 +86,8 @@ __export(config_exports, {
48
86
  migrateConfig: () => migrateConfig,
49
87
  saveConfig: () => saveConfig
50
88
  });
51
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
52
- import { readFileSync, existsSync, renameSync } from "fs";
89
+ import { readFile, writeFile } from "fs/promises";
90
+ import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
53
91
  import path from "path";
54
92
  import os from "os";
55
93
  function resolveDataDir() {
@@ -57,7 +95,7 @@ function resolveDataDir() {
57
95
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
58
96
  const newDir = path.join(os.homedir(), ".exe-os");
59
97
  const legacyDir = path.join(os.homedir(), ".exe-mem");
60
- if (!existsSync(newDir) && existsSync(legacyDir)) {
98
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
61
99
  try {
62
100
  renameSync(legacyDir, newDir);
63
101
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -120,9 +158,9 @@ function normalizeAutoUpdate(raw) {
120
158
  }
121
159
  async function loadConfig() {
122
160
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
123
- await mkdir(dir, { recursive: true });
161
+ await ensurePrivateDir(dir);
124
162
  const configPath = path.join(dir, "config.json");
125
- if (!existsSync(configPath)) {
163
+ if (!existsSync2(configPath)) {
126
164
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
127
165
  }
128
166
  const raw = await readFile(configPath, "utf-8");
@@ -135,6 +173,7 @@ async function loadConfig() {
135
173
  `);
136
174
  try {
137
175
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
176
+ await enforcePrivateFile(configPath);
138
177
  } catch {
139
178
  }
140
179
  }
@@ -153,7 +192,7 @@ async function loadConfig() {
153
192
  function loadConfigSync() {
154
193
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
155
194
  const configPath = path.join(dir, "config.json");
156
- if (!existsSync(configPath)) {
195
+ if (!existsSync2(configPath)) {
157
196
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
158
197
  }
159
198
  try {
@@ -171,12 +210,10 @@ function loadConfigSync() {
171
210
  }
172
211
  async function saveConfig(config) {
173
212
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
174
- await mkdir(dir, { recursive: true });
213
+ await ensurePrivateDir(dir);
175
214
  const configPath = path.join(dir, "config.json");
176
215
  await writeFile(configPath, JSON.stringify(config, null, 2) + "\n");
177
- if (config.cloud?.apiKey) {
178
- await chmod(configPath, 384);
179
- }
216
+ await enforcePrivateFile(configPath);
180
217
  }
181
218
  async function loadConfigFrom(configPath) {
182
219
  const raw = await readFile(configPath, "utf-8");
@@ -196,6 +233,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
196
233
  var init_config = __esm({
197
234
  "src/lib/config.ts"() {
198
235
  "use strict";
236
+ init_secure_files();
199
237
  EXE_AI_DIR = resolveDataDir();
200
238
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
201
239
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
@@ -312,10 +350,10 @@ __export(agent_config_exports, {
312
350
  saveAgentConfig: () => saveAgentConfig,
313
351
  setAgentRuntime: () => setAgentRuntime
314
352
  });
315
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync } from "fs";
353
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
316
354
  import path2 from "path";
317
355
  function loadAgentConfig() {
318
- if (!existsSync2(AGENT_CONFIG_PATH)) return {};
356
+ if (!existsSync3(AGENT_CONFIG_PATH)) return {};
319
357
  try {
320
358
  return JSON.parse(readFileSync2(AGENT_CONFIG_PATH, "utf-8"));
321
359
  } catch {
@@ -324,8 +362,9 @@ function loadAgentConfig() {
324
362
  }
325
363
  function saveAgentConfig(config) {
326
364
  const dir = path2.dirname(AGENT_CONFIG_PATH);
327
- if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
365
+ ensurePrivateDirSync(dir);
328
366
  writeFileSync(AGENT_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
367
+ enforcePrivateFileSync(AGENT_CONFIG_PATH);
329
368
  }
330
369
  function getAgentRuntime(agentId) {
331
370
  const config = loadAgentConfig();
@@ -365,6 +404,7 @@ var init_agent_config = __esm({
365
404
  "use strict";
366
405
  init_config();
367
406
  init_runtime_table();
407
+ init_secure_files();
368
408
  AGENT_CONFIG_PATH = path2.join(EXE_AI_DIR, "agent-config.json");
369
409
  KNOWN_RUNTIMES = {
370
410
  claude: ["claude-opus-4", "claude-sonnet-4", "claude-haiku-4.5"],
@@ -412,7 +452,7 @@ __export(employees_exports, {
412
452
  validateEmployeeName: () => validateEmployeeName
413
453
  });
414
454
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
415
- import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
455
+ import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
416
456
  import { execSync } from "child_process";
417
457
  import path3 from "path";
418
458
  import os2 from "os";
@@ -451,7 +491,7 @@ function validateEmployeeName(name) {
451
491
  return { valid: true };
452
492
  }
453
493
  async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
454
- if (!existsSync3(employeesPath)) {
494
+ if (!existsSync4(employeesPath)) {
455
495
  return [];
456
496
  }
457
497
  const raw = await readFile2(employeesPath, "utf-8");
@@ -466,7 +506,7 @@ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
466
506
  await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
467
507
  }
468
508
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
469
- if (!existsSync3(employeesPath)) return [];
509
+ if (!existsSync4(employeesPath)) return [];
470
510
  try {
471
511
  return JSON.parse(readFileSync3(employeesPath, "utf-8"));
472
512
  } catch {
@@ -514,7 +554,7 @@ function appendToCoordinatorTeam(employee) {
514
554
  const coordinator = getCoordinatorEmployee(loadEmployeesSync());
515
555
  if (!coordinator) return;
516
556
  const idPath = path3.join(IDENTITY_DIR, `${coordinator.name}.md`);
517
- if (!existsSync3(idPath)) return;
557
+ if (!existsSync4(idPath)) return;
518
558
  const content = readFileSync3(idPath, "utf-8");
519
559
  if (content.includes(`**${capitalize(employee.name)}`)) return;
520
560
  const teamMatch = content.match(TEAM_SECTION_RE);
@@ -568,9 +608,9 @@ async function normalizeRosterCase(rosterPath) {
568
608
  const identityDir = path3.join(os2.homedir(), ".exe-os", "identity");
569
609
  const oldPath = path3.join(identityDir, `${oldName}.md`);
570
610
  const newPath = path3.join(identityDir, `${emp.name}.md`);
571
- if (existsSync3(oldPath) && !existsSync3(newPath)) {
611
+ if (existsSync4(oldPath) && !existsSync4(newPath)) {
572
612
  renameSync2(oldPath, newPath);
573
- } else if (existsSync3(oldPath) && oldPath !== newPath) {
613
+ } else if (existsSync4(oldPath) && oldPath !== newPath) {
574
614
  const content = readFileSync3(oldPath, "utf-8");
575
615
  writeFileSync2(newPath, content, "utf-8");
576
616
  if (oldPath.toLowerCase() !== newPath.toLowerCase()) {
@@ -613,7 +653,7 @@ function registerBinSymlinks(name) {
613
653
  for (const suffix of ["", "-opencode"]) {
614
654
  const linkName = `${name}${suffix}`;
615
655
  const linkPath = path3.join(binDir, linkName);
616
- if (existsSync3(linkPath)) {
656
+ if (existsSync4(linkPath)) {
617
657
  skipped.push(linkName);
618
658
  continue;
619
659
  }
@@ -691,119 +731,12 @@ var init_database = __esm({
691
731
  }
692
732
  });
693
733
 
694
- // src/lib/notifications.ts
695
- import crypto from "crypto";
734
+ // src/lib/session-registry.ts
735
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
696
736
  import path5 from "path";
697
737
  import os4 from "os";
698
- import {
699
- readFileSync as readFileSync4,
700
- readdirSync,
701
- unlinkSync as unlinkSync2,
702
- existsSync as existsSync4,
703
- rmdirSync
704
- } from "fs";
705
- async function writeNotification(notification) {
706
- try {
707
- const client = getClient();
708
- const id = crypto.randomUUID();
709
- const now = (/* @__PURE__ */ new Date()).toISOString();
710
- await client.execute({
711
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
712
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
713
- args: [
714
- id,
715
- notification.agentId,
716
- notification.agentRole,
717
- notification.event,
718
- notification.project,
719
- notification.summary,
720
- notification.taskFile ?? null,
721
- now
722
- ]
723
- });
724
- } catch (err) {
725
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
726
- `);
727
- }
728
- }
729
- async function markAsReadByTaskFile(taskFile) {
730
- try {
731
- const client = getClient();
732
- await client.execute({
733
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
734
- args: [taskFile]
735
- });
736
- } catch {
737
- }
738
- }
739
- var init_notifications = __esm({
740
- "src/lib/notifications.ts"() {
741
- "use strict";
742
- init_database();
743
- }
744
- });
745
-
746
- // src/lib/state-bus.ts
747
- var StateBus, orgBus;
748
- var init_state_bus = __esm({
749
- "src/lib/state-bus.ts"() {
750
- "use strict";
751
- StateBus = class {
752
- handlers = /* @__PURE__ */ new Map();
753
- globalHandlers = /* @__PURE__ */ new Set();
754
- /** Emit an event to all subscribers */
755
- emit(event) {
756
- const typeHandlers = this.handlers.get(event.type);
757
- if (typeHandlers) {
758
- for (const handler of typeHandlers) {
759
- try {
760
- handler(event);
761
- } catch {
762
- }
763
- }
764
- }
765
- for (const handler of this.globalHandlers) {
766
- try {
767
- handler(event);
768
- } catch {
769
- }
770
- }
771
- }
772
- /** Subscribe to a specific event type */
773
- on(type, handler) {
774
- if (!this.handlers.has(type)) {
775
- this.handlers.set(type, /* @__PURE__ */ new Set());
776
- }
777
- this.handlers.get(type).add(handler);
778
- }
779
- /** Subscribe to ALL events */
780
- onAny(handler) {
781
- this.globalHandlers.add(handler);
782
- }
783
- /** Unsubscribe from a specific event type */
784
- off(type, handler) {
785
- this.handlers.get(type)?.delete(handler);
786
- }
787
- /** Unsubscribe from ALL events */
788
- offAny(handler) {
789
- this.globalHandlers.delete(handler);
790
- }
791
- /** Remove all listeners */
792
- clear() {
793
- this.handlers.clear();
794
- this.globalHandlers.clear();
795
- }
796
- };
797
- orgBus = new StateBus();
798
- }
799
- });
800
-
801
- // src/lib/session-registry.ts
802
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
803
- import path6 from "path";
804
- import os5 from "os";
805
738
  function registerSession(entry) {
806
- const dir = path6.dirname(REGISTRY_PATH);
739
+ const dir = path5.dirname(REGISTRY_PATH);
807
740
  if (!existsSync5(dir)) {
808
741
  mkdirSync2(dir, { recursive: true });
809
742
  }
@@ -818,7 +751,7 @@ function registerSession(entry) {
818
751
  }
819
752
  function listSessions() {
820
753
  try {
821
- const raw = readFileSync5(REGISTRY_PATH, "utf8");
754
+ const raw = readFileSync4(REGISTRY_PATH, "utf8");
822
755
  return JSON.parse(raw);
823
756
  } catch {
824
757
  return [];
@@ -828,7 +761,7 @@ var REGISTRY_PATH;
828
761
  var init_session_registry = __esm({
829
762
  "src/lib/session-registry.ts"() {
830
763
  "use strict";
831
- REGISTRY_PATH = path6.join(os5.homedir(), ".exe-os", "session-registry.json");
764
+ REGISTRY_PATH = path5.join(os4.homedir(), ".exe-os", "session-registry.json");
832
765
  }
833
766
  });
834
767
 
@@ -1089,17 +1022,17 @@ __export(intercom_queue_exports, {
1089
1022
  queueIntercom: () => queueIntercom,
1090
1023
  readQueue: () => readQueue
1091
1024
  });
1092
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
1093
- import path7 from "path";
1094
- import os6 from "os";
1025
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
1026
+ import path6 from "path";
1027
+ import os5 from "os";
1095
1028
  function ensureDir() {
1096
- const dir = path7.dirname(QUEUE_PATH);
1029
+ const dir = path6.dirname(QUEUE_PATH);
1097
1030
  if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
1098
1031
  }
1099
1032
  function readQueue() {
1100
1033
  try {
1101
1034
  if (!existsSync6(QUEUE_PATH)) return [];
1102
- return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
1035
+ return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
1103
1036
  } catch {
1104
1037
  return [];
1105
1038
  }
@@ -1199,26 +1132,29 @@ var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
1199
1132
  var init_intercom_queue = __esm({
1200
1133
  "src/lib/intercom-queue.ts"() {
1201
1134
  "use strict";
1202
- QUEUE_PATH = path7.join(os6.homedir(), ".exe-os", "intercom-queue.json");
1135
+ QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
1203
1136
  MAX_RETRIES = 5;
1204
1137
  TTL_MS = 60 * 60 * 1e3;
1205
- INTERCOM_LOG = path7.join(os6.homedir(), ".exe-os", "intercom.log");
1138
+ INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
1206
1139
  }
1207
1140
  });
1208
1141
 
1209
1142
  // src/lib/license.ts
1210
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
1143
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
1211
1144
  import { randomUUID } from "crypto";
1212
- import path8 from "path";
1145
+ import { createRequire as createRequire2 } from "module";
1146
+ import { pathToFileURL as pathToFileURL2 } from "url";
1147
+ import os6 from "os";
1148
+ import path7 from "path";
1213
1149
  import { jwtVerify, importSPKI } from "jose";
1214
1150
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
1215
1151
  var init_license = __esm({
1216
1152
  "src/lib/license.ts"() {
1217
1153
  "use strict";
1218
1154
  init_config();
1219
- LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
1220
- CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
1221
- DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
1155
+ LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
1156
+ CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
1157
+ DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
1222
1158
  PLAN_LIMITS = {
1223
1159
  free: { devices: 1, employees: 1, memories: 5e3 },
1224
1160
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -1230,12 +1166,12 @@ var init_license = __esm({
1230
1166
  });
1231
1167
 
1232
1168
  // src/lib/plan-limits.ts
1233
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
1234
- import path9 from "path";
1169
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
1170
+ import path8 from "path";
1235
1171
  function getLicenseSync() {
1236
1172
  try {
1237
1173
  if (!existsSync8(CACHE_PATH2)) return freeLicense();
1238
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
1174
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
1239
1175
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
1240
1176
  const parts = raw.token.split(".");
1241
1177
  if (parts.length !== 3) return freeLicense();
@@ -1274,7 +1210,7 @@ function assertEmployeeLimitSync(rosterPath) {
1274
1210
  let count = 0;
1275
1211
  try {
1276
1212
  if (existsSync8(filePath)) {
1277
- const raw = readFileSync8(filePath, "utf8");
1213
+ const raw = readFileSync7(filePath, "utf8");
1278
1214
  const employees = JSON.parse(raw);
1279
1215
  count = Array.isArray(employees) ? employees.length : 0;
1280
1216
  }
@@ -1303,12 +1239,12 @@ var init_plan_limits = __esm({
1303
1239
  this.name = "PlanLimitError";
1304
1240
  }
1305
1241
  };
1306
- CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
1242
+ CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
1307
1243
  }
1308
1244
  });
1309
1245
 
1310
1246
  // src/lib/session-kill-telemetry.ts
1311
- import crypto2 from "crypto";
1247
+ import crypto from "crypto";
1312
1248
  async function recordSessionKill(input) {
1313
1249
  try {
1314
1250
  const client = getClient();
@@ -1318,7 +1254,7 @@ async function recordSessionKill(input) {
1318
1254
  ticks_idle, estimated_tokens_saved)
1319
1255
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
1320
1256
  args: [
1321
- crypto2.randomUUID(),
1257
+ crypto.randomUUID(),
1322
1258
  input.sessionName,
1323
1259
  input.agentId,
1324
1260
  (/* @__PURE__ */ new Date()).toISOString(),
@@ -1641,6 +1577,7 @@ __export(tmux_routing_exports, {
1641
1577
  isEmployeeAlive: () => isEmployeeAlive,
1642
1578
  isExeSession: () => isExeSession,
1643
1579
  isSessionBusy: () => isSessionBusy,
1580
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
1644
1581
  notifyParentExe: () => notifyParentExe,
1645
1582
  parseParentExe: () => parseParentExe,
1646
1583
  registerParentExe: () => registerParentExe,
@@ -1651,13 +1588,13 @@ __export(tmux_routing_exports, {
1651
1588
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
1652
1589
  });
1653
1590
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
1654
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync as readdirSync2 } from "fs";
1655
- import path10 from "path";
1591
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync } from "fs";
1592
+ import path9 from "path";
1656
1593
  import os7 from "os";
1657
1594
  import { fileURLToPath } from "url";
1658
- import { unlinkSync as unlinkSync3 } from "fs";
1595
+ import { unlinkSync as unlinkSync2 } from "fs";
1659
1596
  function spawnLockPath(sessionName) {
1660
- return path10.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1597
+ return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1661
1598
  }
1662
1599
  function isProcessAlive(pid) {
1663
1600
  try {
@@ -1674,7 +1611,7 @@ function acquireSpawnLock(sessionName) {
1674
1611
  const lockFile = spawnLockPath(sessionName);
1675
1612
  if (existsSync9(lockFile)) {
1676
1613
  try {
1677
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
1614
+ const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1678
1615
  const age = Date.now() - lock.timestamp;
1679
1616
  if (isProcessAlive(lock.pid) && age < 6e4) {
1680
1617
  return false;
@@ -1687,15 +1624,15 @@ function acquireSpawnLock(sessionName) {
1687
1624
  }
1688
1625
  function releaseSpawnLock(sessionName) {
1689
1626
  try {
1690
- unlinkSync3(spawnLockPath(sessionName));
1627
+ unlinkSync2(spawnLockPath(sessionName));
1691
1628
  } catch {
1692
1629
  }
1693
1630
  }
1694
1631
  function resolveBehaviorsExporterScript() {
1695
1632
  try {
1696
1633
  const thisFile = fileURLToPath(import.meta.url);
1697
- const scriptPath = path10.join(
1698
- path10.dirname(thisFile),
1634
+ const scriptPath = path9.join(
1635
+ path9.dirname(thisFile),
1699
1636
  "..",
1700
1637
  "bin",
1701
1638
  "exe-export-behaviors.js"
@@ -1770,7 +1707,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1770
1707
  mkdirSync5(SESSION_CACHE, { recursive: true });
1771
1708
  }
1772
1709
  const rootExe = extractRootExe(parentExe) ?? parentExe;
1773
- const filePath = path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1710
+ const filePath = path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1774
1711
  writeFileSync6(filePath, JSON.stringify({
1775
1712
  parentExe: rootExe,
1776
1713
  dispatchedBy: dispatchedBy || rootExe,
@@ -1779,7 +1716,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1779
1716
  }
1780
1717
  function getParentExe(sessionKey) {
1781
1718
  try {
1782
- const data = JSON.parse(readFileSync9(path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1719
+ const data = JSON.parse(readFileSync8(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1783
1720
  return data.parentExe || null;
1784
1721
  } catch {
1785
1722
  return null;
@@ -1787,8 +1724,8 @@ function getParentExe(sessionKey) {
1787
1724
  }
1788
1725
  function getDispatchedBy(sessionKey) {
1789
1726
  try {
1790
- const data = JSON.parse(readFileSync9(
1791
- path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1727
+ const data = JSON.parse(readFileSync8(
1728
+ path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1792
1729
  "utf8"
1793
1730
  ));
1794
1731
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -1859,7 +1796,7 @@ async function verifyPaneAtCapacity(sessionName) {
1859
1796
  function readDebounceState() {
1860
1797
  try {
1861
1798
  if (!existsSync9(DEBOUNCE_FILE)) return {};
1862
- const raw = JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
1799
+ const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
1863
1800
  const state = {};
1864
1801
  for (const [key, val] of Object.entries(raw)) {
1865
1802
  if (typeof val === "number") {
@@ -1974,7 +1911,7 @@ function sendIntercom(targetSession) {
1974
1911
  try {
1975
1912
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
1976
1913
  const agent = baseAgentName(rawAgent);
1977
- const markerPath = path10.join(SESSION_CACHE, `current-task-${agent}.json`);
1914
+ const markerPath = path9.join(SESSION_CACHE, `current-task-${agent}.json`);
1978
1915
  if (existsSync9(markerPath)) {
1979
1916
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
1980
1917
  return "debounced";
@@ -1984,9 +1921,9 @@ function sendIntercom(targetSession) {
1984
1921
  try {
1985
1922
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
1986
1923
  const agent = baseAgentName(rawAgent);
1987
- const taskDir = path10.join(process.cwd(), "exe", agent);
1924
+ const taskDir = path9.join(process.cwd(), "exe", agent);
1988
1925
  if (existsSync9(taskDir)) {
1989
- const files = readdirSync2(taskDir).filter(
1926
+ const files = readdirSync(taskDir).filter(
1990
1927
  (f) => f.endsWith(".md") && f !== "DONE.txt"
1991
1928
  );
1992
1929
  if (files.length === 0) {
@@ -2045,6 +1982,21 @@ function notifyParentExe(sessionKey) {
2045
1982
  }
2046
1983
  return true;
2047
1984
  }
1985
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
1986
+ const transport = getTransport();
1987
+ try {
1988
+ const sessions = transport.listSessions();
1989
+ if (!sessions.includes(coordinatorSession)) return false;
1990
+ execSync4(
1991
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
1992
+ { timeout: 3e3 }
1993
+ );
1994
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
1995
+ return true;
1996
+ } catch {
1997
+ return false;
1998
+ }
1999
+ }
2048
2000
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2049
2001
  if (isCoordinatorName(employeeName)) {
2050
2002
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -2118,8 +2070,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2118
2070
  const transport = getTransport();
2119
2071
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
2120
2072
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
2121
- const logDir = path10.join(os7.homedir(), ".exe-os", "session-logs");
2122
- const logFile = path10.join(logDir, `${instanceLabel}-${Date.now()}.log`);
2073
+ const logDir = path9.join(os7.homedir(), ".exe-os", "session-logs");
2074
+ const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
2123
2075
  if (!existsSync9(logDir)) {
2124
2076
  mkdirSync5(logDir, { recursive: true });
2125
2077
  }
@@ -2127,17 +2079,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2127
2079
  let cleanupSuffix = "";
2128
2080
  try {
2129
2081
  const thisFile = fileURLToPath(import.meta.url);
2130
- const cleanupScript = path10.join(path10.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
2082
+ const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
2131
2083
  if (existsSync9(cleanupScript)) {
2132
2084
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
2133
2085
  }
2134
2086
  } catch {
2135
2087
  }
2136
2088
  try {
2137
- const claudeJsonPath = path10.join(os7.homedir(), ".claude.json");
2089
+ const claudeJsonPath = path9.join(os7.homedir(), ".claude.json");
2138
2090
  let claudeJson = {};
2139
2091
  try {
2140
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
2092
+ claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
2141
2093
  } catch {
2142
2094
  }
2143
2095
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -2149,13 +2101,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2149
2101
  } catch {
2150
2102
  }
2151
2103
  try {
2152
- const settingsDir = path10.join(os7.homedir(), ".claude", "projects");
2104
+ const settingsDir = path9.join(os7.homedir(), ".claude", "projects");
2153
2105
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
2154
- const projSettingsDir = path10.join(settingsDir, normalizedKey);
2155
- const settingsPath = path10.join(projSettingsDir, "settings.json");
2106
+ const projSettingsDir = path9.join(settingsDir, normalizedKey);
2107
+ const settingsPath = path9.join(projSettingsDir, "settings.json");
2156
2108
  let settings = {};
2157
2109
  try {
2158
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
2110
+ settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
2159
2111
  } catch {
2160
2112
  }
2161
2113
  const perms = settings.permissions ?? {};
@@ -2199,7 +2151,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2199
2151
  let behaviorsFlag = "";
2200
2152
  let legacyFallbackWarned = false;
2201
2153
  if (!useExeAgent && !useBinSymlink) {
2202
- const identityPath2 = path10.join(
2154
+ const identityPath2 = path9.join(
2203
2155
  os7.homedir(),
2204
2156
  ".exe-os",
2205
2157
  "identity",
@@ -2215,7 +2167,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2215
2167
  }
2216
2168
  const behaviorsFile = exportBehaviorsSync(
2217
2169
  employeeName,
2218
- path10.basename(spawnCwd),
2170
+ path9.basename(spawnCwd),
2219
2171
  sessionName
2220
2172
  );
2221
2173
  if (behaviorsFile) {
@@ -2230,9 +2182,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2230
2182
  }
2231
2183
  let sessionContextFlag = "";
2232
2184
  try {
2233
- const ctxDir = path10.join(os7.homedir(), ".exe-os", "session-cache");
2185
+ const ctxDir = path9.join(os7.homedir(), ".exe-os", "session-cache");
2234
2186
  mkdirSync5(ctxDir, { recursive: true });
2235
- const ctxFile = path10.join(ctxDir, `session-context-${sessionName}.md`);
2187
+ const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
2236
2188
  const ctxContent = [
2237
2189
  `## Session Context`,
2238
2190
  `You are running in tmux session: ${sessionName}.`,
@@ -2316,7 +2268,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2316
2268
  transport.pipeLog(sessionName, logFile);
2317
2269
  try {
2318
2270
  const mySession = getMySession();
2319
- const dispatchInfo = path10.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
2271
+ const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
2320
2272
  writeFileSync6(dispatchInfo, JSON.stringify({
2321
2273
  dispatchedBy: mySession,
2322
2274
  rootExe: exeSession,
@@ -2391,15 +2343,15 @@ var init_tmux_routing = __esm({
2391
2343
  init_intercom_queue();
2392
2344
  init_plan_limits();
2393
2345
  init_employees();
2394
- SPAWN_LOCK_DIR = path10.join(os7.homedir(), ".exe-os", "spawn-locks");
2395
- SESSION_CACHE = path10.join(os7.homedir(), ".exe-os", "session-cache");
2346
+ SPAWN_LOCK_DIR = path9.join(os7.homedir(), ".exe-os", "spawn-locks");
2347
+ SESSION_CACHE = path9.join(os7.homedir(), ".exe-os", "session-cache");
2396
2348
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
2397
2349
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
2398
2350
  VERIFY_PANE_LINES = 200;
2399
2351
  INTERCOM_DEBOUNCE_MS = 3e4;
2400
2352
  CODEX_DEBOUNCE_MS = 12e4;
2401
- INTERCOM_LOG2 = path10.join(os7.homedir(), ".exe-os", "intercom.log");
2402
- DEBOUNCE_FILE = path10.join(SESSION_CACHE, "intercom-debounce.json");
2353
+ INTERCOM_LOG2 = path9.join(os7.homedir(), ".exe-os", "intercom.log");
2354
+ DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
2403
2355
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
2404
2356
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
2405
2357
  }
@@ -2422,6 +2374,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
2422
2374
  args: [scope]
2423
2375
  };
2424
2376
  }
2377
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
2378
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
2379
+ if (!scope) return { sql: "", args: [] };
2380
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
2381
+ return {
2382
+ sql: ` AND ${col} = ?`,
2383
+ args: [scope]
2384
+ };
2385
+ }
2425
2386
  var init_task_scope = __esm({
2426
2387
  "src/lib/task-scope.ts"() {
2427
2388
  "use strict";
@@ -2429,13 +2390,125 @@ var init_task_scope = __esm({
2429
2390
  }
2430
2391
  });
2431
2392
 
2393
+ // src/lib/notifications.ts
2394
+ import crypto2 from "crypto";
2395
+ import path10 from "path";
2396
+ import os8 from "os";
2397
+ import {
2398
+ readFileSync as readFileSync9,
2399
+ readdirSync as readdirSync2,
2400
+ unlinkSync as unlinkSync3,
2401
+ existsSync as existsSync10,
2402
+ rmdirSync
2403
+ } from "fs";
2404
+ async function writeNotification(notification) {
2405
+ try {
2406
+ const client = getClient();
2407
+ const id = crypto2.randomUUID();
2408
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2409
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
2410
+ await client.execute({
2411
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
2412
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2413
+ args: [
2414
+ id,
2415
+ notification.agentId,
2416
+ notification.agentRole,
2417
+ notification.event,
2418
+ notification.project,
2419
+ notification.summary,
2420
+ notification.taskFile ?? null,
2421
+ sessionScope,
2422
+ now
2423
+ ]
2424
+ });
2425
+ } catch (err) {
2426
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
2427
+ `);
2428
+ }
2429
+ }
2430
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
2431
+ try {
2432
+ const client = getClient();
2433
+ const scope = strictSessionScopeFilter(sessionScope);
2434
+ await client.execute({
2435
+ sql: `UPDATE notifications SET read = 1
2436
+ WHERE task_file = ? AND read = 0${scope.sql}`,
2437
+ args: [taskFile, ...scope.args]
2438
+ });
2439
+ } catch {
2440
+ }
2441
+ }
2442
+ var init_notifications = __esm({
2443
+ "src/lib/notifications.ts"() {
2444
+ "use strict";
2445
+ init_database();
2446
+ init_task_scope();
2447
+ }
2448
+ });
2449
+
2450
+ // src/lib/state-bus.ts
2451
+ var StateBus, orgBus;
2452
+ var init_state_bus = __esm({
2453
+ "src/lib/state-bus.ts"() {
2454
+ "use strict";
2455
+ StateBus = class {
2456
+ handlers = /* @__PURE__ */ new Map();
2457
+ globalHandlers = /* @__PURE__ */ new Set();
2458
+ /** Emit an event to all subscribers */
2459
+ emit(event) {
2460
+ const typeHandlers = this.handlers.get(event.type);
2461
+ if (typeHandlers) {
2462
+ for (const handler of typeHandlers) {
2463
+ try {
2464
+ handler(event);
2465
+ } catch {
2466
+ }
2467
+ }
2468
+ }
2469
+ for (const handler of this.globalHandlers) {
2470
+ try {
2471
+ handler(event);
2472
+ } catch {
2473
+ }
2474
+ }
2475
+ }
2476
+ /** Subscribe to a specific event type */
2477
+ on(type, handler) {
2478
+ if (!this.handlers.has(type)) {
2479
+ this.handlers.set(type, /* @__PURE__ */ new Set());
2480
+ }
2481
+ this.handlers.get(type).add(handler);
2482
+ }
2483
+ /** Subscribe to ALL events */
2484
+ onAny(handler) {
2485
+ this.globalHandlers.add(handler);
2486
+ }
2487
+ /** Unsubscribe from a specific event type */
2488
+ off(type, handler) {
2489
+ this.handlers.get(type)?.delete(handler);
2490
+ }
2491
+ /** Unsubscribe from ALL events */
2492
+ offAny(handler) {
2493
+ this.globalHandlers.delete(handler);
2494
+ }
2495
+ /** Remove all listeners */
2496
+ clear() {
2497
+ this.handlers.clear();
2498
+ this.globalHandlers.clear();
2499
+ }
2500
+ };
2501
+ orgBus = new StateBus();
2502
+ }
2503
+ });
2504
+
2432
2505
  // src/lib/tasks-crud.ts
2433
2506
  import crypto3 from "crypto";
2434
2507
  import path11 from "path";
2435
- import os8 from "os";
2508
+ import os9 from "os";
2436
2509
  import { execSync as execSync5 } from "child_process";
2437
2510
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2438
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
2511
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2439
2512
  async function writeCheckpoint(input) {
2440
2513
  const client = getClient();
2441
2514
  const row = await resolveTask(client, input.taskId);
@@ -2647,13 +2720,19 @@ ${laneWarning}` : laneWarning;
2647
2720
  });
2648
2721
  if (input.baseDir) {
2649
2722
  try {
2650
- const EXE_OS_DIR = path11.join(os8.homedir(), ".exe-os");
2723
+ const EXE_OS_DIR = path11.join(os9.homedir(), ".exe-os");
2651
2724
  const mdPath = path11.join(EXE_OS_DIR, taskFile);
2652
2725
  const mdDir = path11.dirname(mdPath);
2653
- if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2726
+ if (!existsSync11(mdDir)) await mkdir3(mdDir, { recursive: true });
2654
2727
  const reviewer = input.reviewer ?? input.assignedBy;
2655
2728
  const mdContent = `# ${input.title}
2656
2729
 
2730
+ ## MANDATORY: When done
2731
+
2732
+ You MUST call update_task with status "done" and a result summary when finished.
2733
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2734
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2735
+
2657
2736
  **ID:** ${id}
2658
2737
  **Status:** ${initialStatus}
2659
2738
  **Priority:** ${input.priority}
@@ -2667,12 +2746,6 @@ ${laneWarning}` : laneWarning;
2667
2746
  ## Context
2668
2747
 
2669
2748
  ${input.context}
2670
-
2671
- ## MANDATORY: When done
2672
-
2673
- You MUST call update_task with status "done" and a result summary when finished.
2674
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2675
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
2676
2749
  `;
2677
2750
  await writeFile3(mdPath, mdContent, "utf-8");
2678
2751
  } catch (err) {
@@ -2921,7 +2994,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2921
2994
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
2922
2995
  } catch {
2923
2996
  }
2924
- if (input.status === "done" || input.status === "cancelled") {
2997
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
2925
2998
  try {
2926
2999
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
2927
3000
  clearQueueForAgent2(String(row.assigned_to));
@@ -2952,7 +3025,7 @@ async function deleteTaskCore(taskId, _baseDir) {
2952
3025
  async function ensureArchitectureDoc(baseDir, projectName) {
2953
3026
  const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
2954
3027
  try {
2955
- if (existsSync10(archPath)) return;
3028
+ if (existsSync11(archPath)) return;
2956
3029
  const template = [
2957
3030
  `# ${projectName} \u2014 System Architecture`,
2958
3031
  "",
@@ -2987,7 +3060,7 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2987
3060
  async function ensureGitignoreExe(baseDir) {
2988
3061
  const gitignorePath = path11.join(baseDir, ".gitignore");
2989
3062
  try {
2990
- if (existsSync10(gitignorePath)) {
3063
+ if (existsSync11(gitignorePath)) {
2991
3064
  const content = readFileSync10(gitignorePath, "utf-8");
2992
3065
  if (/^\/?exe\/?$/m.test(content)) return;
2993
3066
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -3020,57 +3093,41 @@ var init_tasks_crud = __esm({
3020
3093
 
3021
3094
  // src/lib/tasks-review.ts
3022
3095
  import path12 from "path";
3023
- import { existsSync as existsSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
3096
+ import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
3024
3097
  async function countPendingReviews(sessionScope) {
3025
3098
  const client = getClient();
3026
- if (sessionScope) {
3027
- const result2 = await client.execute({
3028
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND session_scope = ?",
3029
- args: [sessionScope]
3030
- });
3031
- return Number(result2.rows[0]?.cnt) || 0;
3032
- }
3099
+ const scope = strictSessionScopeFilter(
3100
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3101
+ );
3033
3102
  const result = await client.execute({
3034
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3035
- args: []
3103
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3104
+ WHERE status = 'needs_review'${scope.sql}`,
3105
+ args: [...scope.args]
3036
3106
  });
3037
3107
  return Number(result.rows[0]?.cnt) || 0;
3038
3108
  }
3039
3109
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3040
3110
  const client = getClient();
3041
- if (sessionScope) {
3042
- const result2 = await client.execute({
3043
- sql: `SELECT COUNT(*) as cnt FROM tasks
3044
- WHERE status = 'needs_review' AND updated_at > ?
3045
- AND session_scope = ?`,
3046
- args: [sinceIso, sessionScope]
3047
- });
3048
- return Number(result2.rows[0]?.cnt) || 0;
3049
- }
3111
+ const scope = strictSessionScopeFilter(
3112
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3113
+ );
3050
3114
  const result = await client.execute({
3051
3115
  sql: `SELECT COUNT(*) as cnt FROM tasks
3052
- WHERE status = 'needs_review' AND updated_at > ?`,
3053
- args: [sinceIso]
3116
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
3117
+ args: [sinceIso, ...scope.args]
3054
3118
  });
3055
3119
  return Number(result.rows[0]?.cnt) || 0;
3056
3120
  }
3057
3121
  async function listPendingReviews(limit, sessionScope) {
3058
3122
  const client = getClient();
3059
- if (sessionScope) {
3060
- const result2 = await client.execute({
3061
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3062
- WHERE status = 'needs_review'
3063
- AND session_scope = ?
3064
- ORDER BY updated_at ASC LIMIT ?`,
3065
- args: [sessionScope, limit]
3066
- });
3067
- return result2.rows;
3068
- }
3123
+ const scope = strictSessionScopeFilter(
3124
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3125
+ );
3069
3126
  const result = await client.execute({
3070
3127
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3071
- WHERE status = 'needs_review'
3128
+ WHERE status = 'needs_review'${scope.sql}
3072
3129
  ORDER BY updated_at ASC LIMIT ?`,
3073
- args: [limit]
3130
+ args: [...scope.args, limit]
3074
3131
  });
3075
3132
  return result.rows;
3076
3133
  }
@@ -3082,7 +3139,7 @@ async function cleanupOrphanedReviews() {
3082
3139
  WHERE status IN ('open', 'needs_review', 'in_progress')
3083
3140
  AND assigned_by = 'system'
3084
3141
  AND title LIKE 'Review:%'
3085
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3142
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
3086
3143
  args: [now]
3087
3144
  });
3088
3145
  const r1b = await client.execute({
@@ -3202,7 +3259,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3202
3259
  }
3203
3260
  try {
3204
3261
  const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3205
- if (existsSync11(cacheDir)) {
3262
+ if (existsSync12(cacheDir)) {
3206
3263
  for (const f of readdirSync3(cacheDir)) {
3207
3264
  if (f.startsWith("review-notified-")) {
3208
3265
  unlinkSync4(path12.join(cacheDir, f));
@@ -3222,6 +3279,7 @@ var init_tasks_review = __esm({
3222
3279
  init_tmux_routing();
3223
3280
  init_session_key();
3224
3281
  init_state_bus();
3282
+ init_task_scope();
3225
3283
  }
3226
3284
  });
3227
3285
 
@@ -3278,7 +3336,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
3278
3336
  const scScope = sessionScopeFilter();
3279
3337
  const remaining = await client.execute({
3280
3338
  sql: `SELECT COUNT(*) as cnt FROM tasks
3281
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
3339
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
3282
3340
  args: [parentTaskId, ...scScope.args]
3283
3341
  });
3284
3342
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -3836,7 +3894,7 @@ async function updateTask(input) {
3836
3894
  if (input.status === "in_progress") {
3837
3895
  mkdirSync6(cacheDir, { recursive: true });
3838
3896
  writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3839
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3897
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
3840
3898
  try {
3841
3899
  unlinkSync5(cachePath);
3842
3900
  } catch {
@@ -3844,10 +3902,10 @@ async function updateTask(input) {
3844
3902
  }
3845
3903
  } catch {
3846
3904
  }
3847
- if (input.status === "done") {
3905
+ if (input.status === "done" || input.status === "closed") {
3848
3906
  await cleanupReviewFile(row, taskFile, input.baseDir);
3849
3907
  }
3850
- if (input.status === "done" || input.status === "cancelled") {
3908
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
3851
3909
  try {
3852
3910
  const client = getClient();
3853
3911
  const taskTitle = String(row.title);
@@ -3863,7 +3921,7 @@ async function updateTask(input) {
3863
3921
  if (!isCoordinatorName(assignedAgent)) {
3864
3922
  try {
3865
3923
  const draftClient = getClient();
3866
- if (input.status === "done") {
3924
+ if (input.status === "done" || input.status === "closed") {
3867
3925
  await draftClient.execute({
3868
3926
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3869
3927
  args: [assignedAgent]
@@ -3880,7 +3938,7 @@ async function updateTask(input) {
3880
3938
  try {
3881
3939
  const client = getClient();
3882
3940
  const cascaded = await client.execute({
3883
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
3941
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
3884
3942
  WHERE parent_task_id = ? AND status = 'needs_review'`,
3885
3943
  args: [now, taskId]
3886
3944
  });
@@ -3893,14 +3951,14 @@ async function updateTask(input) {
3893
3951
  } catch {
3894
3952
  }
3895
3953
  }
3896
- const isTerminal = input.status === "done" || input.status === "needs_review";
3954
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
3897
3955
  if (isTerminal) {
3898
3956
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
3899
3957
  if (!isCoordinator) {
3900
3958
  notifyTaskDone();
3901
3959
  }
3902
3960
  await markTaskNotificationsRead(taskFile);
3903
- if (input.status === "done") {
3961
+ if (input.status === "done" || input.status === "closed") {
3904
3962
  try {
3905
3963
  await cascadeUnblock(taskId, input.baseDir, now);
3906
3964
  } catch {
@@ -3920,7 +3978,7 @@ async function updateTask(input) {
3920
3978
  }
3921
3979
  }
3922
3980
  }
3923
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3981
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3924
3982
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3925
3983
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3926
3984
  taskId,
@@ -4001,12 +4059,12 @@ __export(identity_exports, {
4001
4059
  listIdentities: () => listIdentities,
4002
4060
  updateIdentity: () => updateIdentity
4003
4061
  });
4004
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
4062
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
4005
4063
  import { readdirSync as readdirSync5 } from "fs";
4006
4064
  import path17 from "path";
4007
4065
  import { createHash } from "crypto";
4008
4066
  function ensureDir2() {
4009
- if (!existsSync12(IDENTITY_DIR2)) {
4067
+ if (!existsSync13(IDENTITY_DIR2)) {
4010
4068
  mkdirSync8(IDENTITY_DIR2, { recursive: true });
4011
4069
  }
4012
4070
  }
@@ -4052,7 +4110,7 @@ function contentHash(content) {
4052
4110
  }
4053
4111
  function getIdentity(agentId) {
4054
4112
  const filePath = identityPath(agentId);
4055
- if (!existsSync12(filePath)) return null;
4113
+ if (!existsSync13(filePath)) return null;
4056
4114
  const raw = readFileSync12(filePath, "utf-8");
4057
4115
  const { frontmatter, body } = parseFrontmatter(raw);
4058
4116
  return {
@@ -4828,10 +4886,10 @@ function registerCreateTask(server) {
4828
4886
  skipDispatch: true
4829
4887
  });
4830
4888
  try {
4831
- const { existsSync: existsSync13, mkdirSync: mkdirSync9, writeFileSync: writeFileSync10 } = await import("fs");
4889
+ const { existsSync: existsSync14, mkdirSync: mkdirSync9, writeFileSync: writeFileSync10 } = await import("fs");
4832
4890
  const { identityPath: identityPath2 } = await Promise.resolve().then(() => (init_identity(), identity_exports));
4833
4891
  const idPath = identityPath2(assigned_to);
4834
- if (!existsSync13(idPath)) {
4892
+ if (!existsSync14(idPath)) {
4835
4893
  const { loadEmployees: loadEmployees2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
4836
4894
  const employees = await loadEmployees2();
4837
4895
  const emp = employees.find((e) => e.name === assigned_to);
@@ -4840,7 +4898,7 @@ function registerCreateTask(server) {
4840
4898
  const template = getTemplateForTitle2(emp.role);
4841
4899
  if (template) {
4842
4900
  const dir = (await import("path")).dirname(idPath);
4843
- if (!existsSync13(dir)) mkdirSync9(dir, { recursive: true });
4901
+ if (!existsSync14(dir)) mkdirSync9(dir, { recursive: true });
4844
4902
  writeFileSync10(idPath, template.replace(/^agent_id: \w+/m, `agent_id: ${assigned_to}`), "utf-8");
4845
4903
  }
4846
4904
  }