@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
@@ -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,229 @@ var init_task_scope = __esm({
2429
2390
  }
2430
2391
  });
2431
2392
 
2432
- // src/lib/tasks-crud.ts
2433
- import crypto3 from "crypto";
2434
- import path11 from "path";
2393
+ // src/lib/notifications.ts
2394
+ import crypto2 from "crypto";
2395
+ import path10 from "path";
2435
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
+
2505
+ // src/lib/project-name.ts
2436
2506
  import { execSync as execSync5 } from "child_process";
2507
+ import path11 from "path";
2508
+ function getProjectName(cwd) {
2509
+ const dir = cwd ?? process.cwd();
2510
+ if (_cached2 && _cachedCwd === dir) return _cached2;
2511
+ try {
2512
+ let repoRoot;
2513
+ try {
2514
+ const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
2515
+ cwd: dir,
2516
+ encoding: "utf8",
2517
+ timeout: 2e3,
2518
+ stdio: ["pipe", "pipe", "pipe"]
2519
+ }).trim();
2520
+ repoRoot = path11.dirname(gitCommonDir);
2521
+ } catch {
2522
+ repoRoot = execSync5("git rev-parse --show-toplevel", {
2523
+ cwd: dir,
2524
+ encoding: "utf8",
2525
+ timeout: 2e3,
2526
+ stdio: ["pipe", "pipe", "pipe"]
2527
+ }).trim();
2528
+ }
2529
+ _cached2 = path11.basename(repoRoot);
2530
+ _cachedCwd = dir;
2531
+ return _cached2;
2532
+ } catch {
2533
+ _cached2 = path11.basename(dir);
2534
+ _cachedCwd = dir;
2535
+ return _cached2;
2536
+ }
2537
+ }
2538
+ var _cached2, _cachedCwd;
2539
+ var init_project_name = __esm({
2540
+ "src/lib/project-name.ts"() {
2541
+ "use strict";
2542
+ _cached2 = null;
2543
+ _cachedCwd = null;
2544
+ }
2545
+ });
2546
+
2547
+ // src/lib/session-scope.ts
2548
+ var session_scope_exports = {};
2549
+ __export(session_scope_exports, {
2550
+ assertSessionScope: () => assertSessionScope,
2551
+ findSessionForProject: () => findSessionForProject,
2552
+ getSessionProject: () => getSessionProject
2553
+ });
2554
+ function getSessionProject(sessionName) {
2555
+ const sessions = listSessions();
2556
+ const entry = sessions.find((s) => s.windowName === sessionName);
2557
+ if (!entry) return null;
2558
+ const parts = entry.projectDir.split("/").filter(Boolean);
2559
+ return parts[parts.length - 1] ?? null;
2560
+ }
2561
+ function findSessionForProject(projectName) {
2562
+ const sessions = listSessions();
2563
+ for (const s of sessions) {
2564
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
2565
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
2566
+ }
2567
+ return null;
2568
+ }
2569
+ function assertSessionScope(actionType, targetProject) {
2570
+ try {
2571
+ const currentProject = getProjectName();
2572
+ const exeSession = resolveExeSession();
2573
+ if (!exeSession) {
2574
+ return { allowed: true, reason: "no_session" };
2575
+ }
2576
+ if (currentProject === targetProject) {
2577
+ return {
2578
+ allowed: true,
2579
+ reason: "same_session",
2580
+ currentProject,
2581
+ targetProject
2582
+ };
2583
+ }
2584
+ process.stderr.write(
2585
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
2586
+ `
2587
+ );
2588
+ return {
2589
+ allowed: false,
2590
+ reason: "cross_session_denied",
2591
+ currentProject,
2592
+ targetProject,
2593
+ targetSession: findSessionForProject(targetProject)?.windowName
2594
+ };
2595
+ } catch {
2596
+ return { allowed: true, reason: "no_session" };
2597
+ }
2598
+ }
2599
+ var init_session_scope = __esm({
2600
+ "src/lib/session-scope.ts"() {
2601
+ "use strict";
2602
+ init_session_registry();
2603
+ init_project_name();
2604
+ init_tmux_routing();
2605
+ init_employees();
2606
+ }
2607
+ });
2608
+
2609
+ // src/lib/tasks-crud.ts
2610
+ import crypto3 from "crypto";
2611
+ import path12 from "path";
2612
+ import os9 from "os";
2613
+ import { execSync as execSync6 } from "child_process";
2437
2614
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2438
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
2615
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2439
2616
  async function writeCheckpoint(input) {
2440
2617
  const client = getClient();
2441
2618
  const row = await resolveTask(client, input.taskId);
@@ -2555,9 +2732,24 @@ async function createTaskCore(input) {
2555
2732
  const now = (/* @__PURE__ */ new Date()).toISOString();
2556
2733
  const slug = slugify(input.title);
2557
2734
  let earlySessionScope = null;
2735
+ let scopeMismatchWarning;
2558
2736
  try {
2559
2737
  const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2560
- earlySessionScope = resolveExeSession2();
2738
+ const resolved = resolveExeSession2();
2739
+ if (resolved && input.projectName) {
2740
+ const { getSessionProject: getSessionProject2 } = await Promise.resolve().then(() => (init_session_scope(), session_scope_exports));
2741
+ const sessionProject = getSessionProject2(resolved);
2742
+ if (sessionProject && sessionProject !== input.projectName) {
2743
+ scopeMismatchWarning = `session/project mismatch: session "${resolved}" owns "${sessionProject}" but task targets "${input.projectName}". Routed to default scope.`;
2744
+ process.stderr.write(`[create_task] ${scopeMismatchWarning}
2745
+ `);
2746
+ earlySessionScope = null;
2747
+ } else {
2748
+ earlySessionScope = resolved;
2749
+ }
2750
+ } else {
2751
+ earlySessionScope = resolved;
2752
+ }
2561
2753
  } catch {
2562
2754
  }
2563
2755
  const scope = earlySessionScope ?? "default";
@@ -2608,10 +2800,14 @@ async function createTaskCore(input) {
2608
2800
  ${laneWarning}` : laneWarning;
2609
2801
  }
2610
2802
  }
2803
+ if (scopeMismatchWarning) {
2804
+ warning = warning ? `${warning}
2805
+ ${scopeMismatchWarning}` : scopeMismatchWarning;
2806
+ }
2611
2807
  if (input.baseDir) {
2612
2808
  try {
2613
- await mkdir3(path11.join(input.baseDir, "exe", "output"), { recursive: true });
2614
- await mkdir3(path11.join(input.baseDir, "exe", "research"), { recursive: true });
2809
+ await mkdir3(path12.join(input.baseDir, "exe", "output"), { recursive: true });
2810
+ await mkdir3(path12.join(input.baseDir, "exe", "research"), { recursive: true });
2615
2811
  await ensureArchitectureDoc(input.baseDir, input.projectName);
2616
2812
  await ensureGitignoreExe(input.baseDir);
2617
2813
  } catch {
@@ -2647,13 +2843,19 @@ ${laneWarning}` : laneWarning;
2647
2843
  });
2648
2844
  if (input.baseDir) {
2649
2845
  try {
2650
- const EXE_OS_DIR = path11.join(os8.homedir(), ".exe-os");
2651
- const mdPath = path11.join(EXE_OS_DIR, taskFile);
2652
- const mdDir = path11.dirname(mdPath);
2653
- if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2846
+ const EXE_OS_DIR = path12.join(os9.homedir(), ".exe-os");
2847
+ const mdPath = path12.join(EXE_OS_DIR, taskFile);
2848
+ const mdDir = path12.dirname(mdPath);
2849
+ if (!existsSync11(mdDir)) await mkdir3(mdDir, { recursive: true });
2654
2850
  const reviewer = input.reviewer ?? input.assignedBy;
2655
2851
  const mdContent = `# ${input.title}
2656
2852
 
2853
+ ## MANDATORY: When done
2854
+
2855
+ You MUST call update_task with status "done" and a result summary when finished.
2856
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2857
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2858
+
2657
2859
  **ID:** ${id}
2658
2860
  **Status:** ${initialStatus}
2659
2861
  **Priority:** ${input.priority}
@@ -2667,12 +2869,6 @@ ${laneWarning}` : laneWarning;
2667
2869
  ## Context
2668
2870
 
2669
2871
  ${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
2872
  `;
2677
2873
  await writeFile3(mdPath, mdContent, "utf-8");
2678
2874
  } catch (err) {
@@ -2754,14 +2950,14 @@ function isTmuxSessionAlive(identifier) {
2754
2950
  if (!identifier || identifier === "unknown") return true;
2755
2951
  try {
2756
2952
  if (identifier.startsWith("%")) {
2757
- const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
2953
+ const output = execSync6("tmux list-panes -a -F '#{pane_id}'", {
2758
2954
  timeout: 2e3,
2759
2955
  encoding: "utf8",
2760
2956
  stdio: ["pipe", "pipe", "pipe"]
2761
2957
  });
2762
2958
  return output.split("\n").some((l) => l.trim() === identifier);
2763
2959
  } else {
2764
- execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2960
+ execSync6(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2765
2961
  timeout: 2e3,
2766
2962
  stdio: ["pipe", "pipe", "pipe"]
2767
2963
  });
@@ -2770,7 +2966,7 @@ function isTmuxSessionAlive(identifier) {
2770
2966
  } catch {
2771
2967
  if (identifier.startsWith("%")) return true;
2772
2968
  try {
2773
- execSync5("tmux list-sessions", {
2969
+ execSync6("tmux list-sessions", {
2774
2970
  timeout: 2e3,
2775
2971
  stdio: ["pipe", "pipe", "pipe"]
2776
2972
  });
@@ -2785,12 +2981,12 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
2785
2981
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
2786
2982
  try {
2787
2983
  const since = new Date(taskCreatedAt).toISOString();
2788
- const branch = execSync5(
2984
+ const branch = execSync6(
2789
2985
  "git rev-parse --abbrev-ref HEAD 2>/dev/null",
2790
2986
  { encoding: "utf8", timeout: 3e3 }
2791
2987
  ).trim();
2792
2988
  const branchArg = branch && branch !== "HEAD" ? branch : "";
2793
- const commitCount = execSync5(
2989
+ const commitCount = execSync6(
2794
2990
  `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
2795
2991
  { encoding: "utf8", timeout: 5e3 }
2796
2992
  ).trim();
@@ -2921,7 +3117,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2921
3117
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
2922
3118
  } catch {
2923
3119
  }
2924
- if (input.status === "done" || input.status === "cancelled") {
3120
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
2925
3121
  try {
2926
3122
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
2927
3123
  clearQueueForAgent2(String(row.assigned_to));
@@ -2950,9 +3146,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2950
3146
  return { taskFile, assignedTo, assignedBy, taskSlug };
2951
3147
  }
2952
3148
  async function ensureArchitectureDoc(baseDir, projectName) {
2953
- const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
3149
+ const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
2954
3150
  try {
2955
- if (existsSync10(archPath)) return;
3151
+ if (existsSync11(archPath)) return;
2956
3152
  const template = [
2957
3153
  `# ${projectName} \u2014 System Architecture`,
2958
3154
  "",
@@ -2985,9 +3181,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2985
3181
  }
2986
3182
  }
2987
3183
  async function ensureGitignoreExe(baseDir) {
2988
- const gitignorePath = path11.join(baseDir, ".gitignore");
3184
+ const gitignorePath = path12.join(baseDir, ".gitignore");
2989
3185
  try {
2990
- if (existsSync10(gitignorePath)) {
3186
+ if (existsSync11(gitignorePath)) {
2991
3187
  const content = readFileSync10(gitignorePath, "utf-8");
2992
3188
  if (/^\/?exe\/?$/m.test(content)) return;
2993
3189
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -3019,58 +3215,42 @@ var init_tasks_crud = __esm({
3019
3215
  });
3020
3216
 
3021
3217
  // src/lib/tasks-review.ts
3022
- import path12 from "path";
3023
- import { existsSync as existsSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
3218
+ import path13 from "path";
3219
+ import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
3024
3220
  async function countPendingReviews(sessionScope) {
3025
3221
  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
- }
3222
+ const scope = strictSessionScopeFilter(
3223
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3224
+ );
3033
3225
  const result = await client.execute({
3034
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
3035
- args: []
3226
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3227
+ WHERE status = 'needs_review'${scope.sql}`,
3228
+ args: [...scope.args]
3036
3229
  });
3037
3230
  return Number(result.rows[0]?.cnt) || 0;
3038
3231
  }
3039
3232
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3040
3233
  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
- }
3234
+ const scope = strictSessionScopeFilter(
3235
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3236
+ );
3050
3237
  const result = await client.execute({
3051
3238
  sql: `SELECT COUNT(*) as cnt FROM tasks
3052
- WHERE status = 'needs_review' AND updated_at > ?`,
3053
- args: [sinceIso]
3239
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
3240
+ args: [sinceIso, ...scope.args]
3054
3241
  });
3055
3242
  return Number(result.rows[0]?.cnt) || 0;
3056
3243
  }
3057
3244
  async function listPendingReviews(limit, sessionScope) {
3058
3245
  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
- }
3246
+ const scope = strictSessionScopeFilter(
3247
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3248
+ );
3069
3249
  const result = await client.execute({
3070
3250
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3071
- WHERE status = 'needs_review'
3251
+ WHERE status = 'needs_review'${scope.sql}
3072
3252
  ORDER BY updated_at ASC LIMIT ?`,
3073
- args: [limit]
3253
+ args: [...scope.args, limit]
3074
3254
  });
3075
3255
  return result.rows;
3076
3256
  }
@@ -3082,7 +3262,7 @@ async function cleanupOrphanedReviews() {
3082
3262
  WHERE status IN ('open', 'needs_review', 'in_progress')
3083
3263
  AND assigned_by = 'system'
3084
3264
  AND title LIKE 'Review:%'
3085
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3265
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
3086
3266
  args: [now]
3087
3267
  });
3088
3268
  const r1b = await client.execute({
@@ -3201,11 +3381,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3201
3381
  );
3202
3382
  }
3203
3383
  try {
3204
- const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3205
- if (existsSync11(cacheDir)) {
3384
+ const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3385
+ if (existsSync12(cacheDir)) {
3206
3386
  for (const f of readdirSync3(cacheDir)) {
3207
3387
  if (f.startsWith("review-notified-")) {
3208
- unlinkSync4(path12.join(cacheDir, f));
3388
+ unlinkSync4(path13.join(cacheDir, f));
3209
3389
  }
3210
3390
  }
3211
3391
  }
@@ -3222,11 +3402,12 @@ var init_tasks_review = __esm({
3222
3402
  init_tmux_routing();
3223
3403
  init_session_key();
3224
3404
  init_state_bus();
3405
+ init_task_scope();
3225
3406
  }
3226
3407
  });
3227
3408
 
3228
3409
  // src/lib/tasks-chain.ts
3229
- import path13 from "path";
3410
+ import path14 from "path";
3230
3411
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
3231
3412
  async function cascadeUnblock(taskId, baseDir, now) {
3232
3413
  const client = getClient();
@@ -3243,7 +3424,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
3243
3424
  });
3244
3425
  for (const ur of unblockedRows.rows) {
3245
3426
  try {
3246
- const ubFile = path13.join(baseDir, String(ur.task_file));
3427
+ const ubFile = path14.join(baseDir, String(ur.task_file));
3247
3428
  let ubContent = await readFile3(ubFile, "utf-8");
3248
3429
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
3249
3430
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -3278,7 +3459,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
3278
3459
  const scScope = sessionScopeFilter();
3279
3460
  const remaining = await client.execute({
3280
3461
  sql: `SELECT COUNT(*) as cnt FROM tasks
3281
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
3462
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
3282
3463
  args: [parentTaskId, ...scScope.args]
3283
3464
  });
3284
3465
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -3310,110 +3491,6 @@ var init_tasks_chain = __esm({
3310
3491
  }
3311
3492
  });
3312
3493
 
3313
- // src/lib/project-name.ts
3314
- import { execSync as execSync6 } from "child_process";
3315
- import path14 from "path";
3316
- function getProjectName(cwd) {
3317
- const dir = cwd ?? process.cwd();
3318
- if (_cached2 && _cachedCwd === dir) return _cached2;
3319
- try {
3320
- let repoRoot;
3321
- try {
3322
- const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
3323
- cwd: dir,
3324
- encoding: "utf8",
3325
- timeout: 2e3,
3326
- stdio: ["pipe", "pipe", "pipe"]
3327
- }).trim();
3328
- repoRoot = path14.dirname(gitCommonDir);
3329
- } catch {
3330
- repoRoot = execSync6("git rev-parse --show-toplevel", {
3331
- cwd: dir,
3332
- encoding: "utf8",
3333
- timeout: 2e3,
3334
- stdio: ["pipe", "pipe", "pipe"]
3335
- }).trim();
3336
- }
3337
- _cached2 = path14.basename(repoRoot);
3338
- _cachedCwd = dir;
3339
- return _cached2;
3340
- } catch {
3341
- _cached2 = path14.basename(dir);
3342
- _cachedCwd = dir;
3343
- return _cached2;
3344
- }
3345
- }
3346
- var _cached2, _cachedCwd;
3347
- var init_project_name = __esm({
3348
- "src/lib/project-name.ts"() {
3349
- "use strict";
3350
- _cached2 = null;
3351
- _cachedCwd = null;
3352
- }
3353
- });
3354
-
3355
- // src/lib/session-scope.ts
3356
- var session_scope_exports = {};
3357
- __export(session_scope_exports, {
3358
- assertSessionScope: () => assertSessionScope,
3359
- findSessionForProject: () => findSessionForProject,
3360
- getSessionProject: () => getSessionProject
3361
- });
3362
- function getSessionProject(sessionName) {
3363
- const sessions = listSessions();
3364
- const entry = sessions.find((s) => s.windowName === sessionName);
3365
- if (!entry) return null;
3366
- const parts = entry.projectDir.split("/").filter(Boolean);
3367
- return parts[parts.length - 1] ?? null;
3368
- }
3369
- function findSessionForProject(projectName) {
3370
- const sessions = listSessions();
3371
- for (const s of sessions) {
3372
- const proj = s.projectDir.split("/").filter(Boolean).pop();
3373
- if (proj === projectName && isCoordinatorName(s.agentId)) return s;
3374
- }
3375
- return null;
3376
- }
3377
- function assertSessionScope(actionType, targetProject) {
3378
- try {
3379
- const currentProject = getProjectName();
3380
- const exeSession = resolveExeSession();
3381
- if (!exeSession) {
3382
- return { allowed: true, reason: "no_session" };
3383
- }
3384
- if (currentProject === targetProject) {
3385
- return {
3386
- allowed: true,
3387
- reason: "same_session",
3388
- currentProject,
3389
- targetProject
3390
- };
3391
- }
3392
- process.stderr.write(
3393
- `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
3394
- `
3395
- );
3396
- return {
3397
- allowed: false,
3398
- reason: "cross_session_denied",
3399
- currentProject,
3400
- targetProject,
3401
- targetSession: findSessionForProject(targetProject)?.windowName
3402
- };
3403
- } catch {
3404
- return { allowed: true, reason: "no_session" };
3405
- }
3406
- }
3407
- var init_session_scope = __esm({
3408
- "src/lib/session-scope.ts"() {
3409
- "use strict";
3410
- init_session_registry();
3411
- init_project_name();
3412
- init_tmux_routing();
3413
- init_employees();
3414
- }
3415
- });
3416
-
3417
3494
  // src/lib/tasks-notify.ts
3418
3495
  async function dispatchTaskToEmployee(input) {
3419
3496
  if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
@@ -3836,7 +3913,7 @@ async function updateTask(input) {
3836
3913
  if (input.status === "in_progress") {
3837
3914
  mkdirSync6(cacheDir, { recursive: true });
3838
3915
  writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3839
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3916
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
3840
3917
  try {
3841
3918
  unlinkSync5(cachePath);
3842
3919
  } catch {
@@ -3844,10 +3921,10 @@ async function updateTask(input) {
3844
3921
  }
3845
3922
  } catch {
3846
3923
  }
3847
- if (input.status === "done") {
3924
+ if (input.status === "done" || input.status === "closed") {
3848
3925
  await cleanupReviewFile(row, taskFile, input.baseDir);
3849
3926
  }
3850
- if (input.status === "done" || input.status === "cancelled") {
3927
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
3851
3928
  try {
3852
3929
  const client = getClient();
3853
3930
  const taskTitle = String(row.title);
@@ -3863,7 +3940,7 @@ async function updateTask(input) {
3863
3940
  if (!isCoordinatorName(assignedAgent)) {
3864
3941
  try {
3865
3942
  const draftClient = getClient();
3866
- if (input.status === "done") {
3943
+ if (input.status === "done" || input.status === "closed") {
3867
3944
  await draftClient.execute({
3868
3945
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3869
3946
  args: [assignedAgent]
@@ -3880,7 +3957,7 @@ async function updateTask(input) {
3880
3957
  try {
3881
3958
  const client = getClient();
3882
3959
  const cascaded = await client.execute({
3883
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
3960
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
3884
3961
  WHERE parent_task_id = ? AND status = 'needs_review'`,
3885
3962
  args: [now, taskId]
3886
3963
  });
@@ -3893,14 +3970,14 @@ async function updateTask(input) {
3893
3970
  } catch {
3894
3971
  }
3895
3972
  }
3896
- const isTerminal = input.status === "done" || input.status === "needs_review";
3973
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
3897
3974
  if (isTerminal) {
3898
3975
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
3899
3976
  if (!isCoordinator) {
3900
3977
  notifyTaskDone();
3901
3978
  }
3902
3979
  await markTaskNotificationsRead(taskFile);
3903
- if (input.status === "done") {
3980
+ if (input.status === "done" || input.status === "closed") {
3904
3981
  try {
3905
3982
  await cascadeUnblock(taskId, input.baseDir, now);
3906
3983
  } catch {
@@ -3920,7 +3997,7 @@ async function updateTask(input) {
3920
3997
  }
3921
3998
  }
3922
3999
  }
3923
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4000
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3924
4001
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3925
4002
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3926
4003
  taskId,
@@ -4001,12 +4078,12 @@ __export(identity_exports, {
4001
4078
  listIdentities: () => listIdentities,
4002
4079
  updateIdentity: () => updateIdentity
4003
4080
  });
4004
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
4081
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
4005
4082
  import { readdirSync as readdirSync5 } from "fs";
4006
4083
  import path17 from "path";
4007
4084
  import { createHash } from "crypto";
4008
4085
  function ensureDir2() {
4009
- if (!existsSync12(IDENTITY_DIR2)) {
4086
+ if (!existsSync13(IDENTITY_DIR2)) {
4010
4087
  mkdirSync8(IDENTITY_DIR2, { recursive: true });
4011
4088
  }
4012
4089
  }
@@ -4052,7 +4129,7 @@ function contentHash(content) {
4052
4129
  }
4053
4130
  function getIdentity(agentId) {
4054
4131
  const filePath = identityPath(agentId);
4055
- if (!existsSync12(filePath)) return null;
4132
+ if (!existsSync13(filePath)) return null;
4056
4133
  const raw = readFileSync12(filePath, "utf-8");
4057
4134
  const { frontmatter, body } = parseFrontmatter(raw);
4058
4135
  return {
@@ -4828,10 +4905,10 @@ function registerCreateTask(server) {
4828
4905
  skipDispatch: true
4829
4906
  });
4830
4907
  try {
4831
- const { existsSync: existsSync13, mkdirSync: mkdirSync9, writeFileSync: writeFileSync10 } = await import("fs");
4908
+ const { existsSync: existsSync14, mkdirSync: mkdirSync9, writeFileSync: writeFileSync10 } = await import("fs");
4832
4909
  const { identityPath: identityPath2 } = await Promise.resolve().then(() => (init_identity(), identity_exports));
4833
4910
  const idPath = identityPath2(assigned_to);
4834
- if (!existsSync13(idPath)) {
4911
+ if (!existsSync14(idPath)) {
4835
4912
  const { loadEmployees: loadEmployees2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
4836
4913
  const employees = await loadEmployees2();
4837
4914
  const emp = employees.find((e) => e.name === assigned_to);
@@ -4840,7 +4917,7 @@ function registerCreateTask(server) {
4840
4917
  const template = getTemplateForTitle2(emp.role);
4841
4918
  if (template) {
4842
4919
  const dir = (await import("path")).dirname(idPath);
4843
- if (!existsSync13(dir)) mkdirSync9(dir, { recursive: true });
4920
+ if (!existsSync14(dir)) mkdirSync9(dir, { recursive: true });
4844
4921
  writeFileSync10(idPath, template.replace(/^agent_id: \w+/m, `agent_id: ${assigned_to}`), "utf-8");
4845
4922
  }
4846
4923
  }