@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
package/dist/lib/tasks.js CHANGED
@@ -32,9 +32,34 @@ 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
+ async function enforcePrivateFile(filePath) {
46
+ try {
47
+ await chmod(filePath, PRIVATE_FILE_MODE);
48
+ } catch {
49
+ }
50
+ }
51
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
52
+ var init_secure_files = __esm({
53
+ "src/lib/secure-files.ts"() {
54
+ "use strict";
55
+ PRIVATE_DIR_MODE = 448;
56
+ PRIVATE_FILE_MODE = 384;
57
+ }
58
+ });
59
+
35
60
  // src/lib/config.ts
36
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
37
- import { readFileSync, existsSync, renameSync } from "fs";
61
+ import { readFile, writeFile } from "fs/promises";
62
+ import { readFileSync, existsSync as existsSync2, renameSync } from "fs";
38
63
  import path from "path";
39
64
  import os from "os";
40
65
  function resolveDataDir() {
@@ -42,7 +67,7 @@ function resolveDataDir() {
42
67
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
43
68
  const newDir = path.join(os.homedir(), ".exe-os");
44
69
  const legacyDir = path.join(os.homedir(), ".exe-mem");
45
- if (!existsSync(newDir) && existsSync(legacyDir)) {
70
+ if (!existsSync2(newDir) && existsSync2(legacyDir)) {
46
71
  try {
47
72
  renameSync(legacyDir, newDir);
48
73
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -105,9 +130,9 @@ function normalizeAutoUpdate(raw) {
105
130
  }
106
131
  async function loadConfig() {
107
132
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
108
- await mkdir(dir, { recursive: true });
133
+ await ensurePrivateDir(dir);
109
134
  const configPath = path.join(dir, "config.json");
110
- if (!existsSync(configPath)) {
135
+ if (!existsSync2(configPath)) {
111
136
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
112
137
  }
113
138
  const raw = await readFile(configPath, "utf-8");
@@ -120,6 +145,7 @@ async function loadConfig() {
120
145
  `);
121
146
  try {
122
147
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
148
+ await enforcePrivateFile(configPath);
123
149
  } catch {
124
150
  }
125
151
  }
@@ -139,6 +165,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
139
165
  var init_config = __esm({
140
166
  "src/lib/config.ts"() {
141
167
  "use strict";
168
+ init_secure_files();
142
169
  EXE_AI_DIR = resolveDataDir();
143
170
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
144
171
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
@@ -217,7 +244,7 @@ var init_config = __esm({
217
244
 
218
245
  // src/lib/employees.ts
219
246
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
220
- import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
247
+ import { existsSync as existsSync3, symlinkSync, readlinkSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
221
248
  import { execSync } from "child_process";
222
249
  import path2 from "path";
223
250
  import os2 from "os";
@@ -238,7 +265,7 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
238
265
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
239
266
  }
240
267
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
241
- if (!existsSync2(employeesPath)) return [];
268
+ if (!existsSync3(employeesPath)) return [];
242
269
  try {
243
270
  return JSON.parse(readFileSync2(employeesPath, "utf-8"));
244
271
  } catch {
@@ -326,121 +353,14 @@ var init_database = __esm({
326
353
  }
327
354
  });
328
355
 
329
- // src/lib/notifications.ts
330
- import crypto from "crypto";
356
+ // src/lib/session-registry.ts
357
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
331
358
  import path4 from "path";
332
359
  import os4 from "os";
333
- import {
334
- readFileSync as readFileSync3,
335
- readdirSync,
336
- unlinkSync as unlinkSync2,
337
- existsSync as existsSync3,
338
- rmdirSync
339
- } from "fs";
340
- async function writeNotification(notification) {
341
- try {
342
- const client = getClient();
343
- const id = crypto.randomUUID();
344
- const now = (/* @__PURE__ */ new Date()).toISOString();
345
- await client.execute({
346
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
347
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
348
- args: [
349
- id,
350
- notification.agentId,
351
- notification.agentRole,
352
- notification.event,
353
- notification.project,
354
- notification.summary,
355
- notification.taskFile ?? null,
356
- now
357
- ]
358
- });
359
- } catch (err) {
360
- process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
361
- `);
362
- }
363
- }
364
- async function markAsReadByTaskFile(taskFile) {
365
- try {
366
- const client = getClient();
367
- await client.execute({
368
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
369
- args: [taskFile]
370
- });
371
- } catch {
372
- }
373
- }
374
- var init_notifications = __esm({
375
- "src/lib/notifications.ts"() {
376
- "use strict";
377
- init_database();
378
- }
379
- });
380
-
381
- // src/lib/state-bus.ts
382
- var StateBus, orgBus;
383
- var init_state_bus = __esm({
384
- "src/lib/state-bus.ts"() {
385
- "use strict";
386
- StateBus = class {
387
- handlers = /* @__PURE__ */ new Map();
388
- globalHandlers = /* @__PURE__ */ new Set();
389
- /** Emit an event to all subscribers */
390
- emit(event) {
391
- const typeHandlers = this.handlers.get(event.type);
392
- if (typeHandlers) {
393
- for (const handler of typeHandlers) {
394
- try {
395
- handler(event);
396
- } catch {
397
- }
398
- }
399
- }
400
- for (const handler of this.globalHandlers) {
401
- try {
402
- handler(event);
403
- } catch {
404
- }
405
- }
406
- }
407
- /** Subscribe to a specific event type */
408
- on(type, handler) {
409
- if (!this.handlers.has(type)) {
410
- this.handlers.set(type, /* @__PURE__ */ new Set());
411
- }
412
- this.handlers.get(type).add(handler);
413
- }
414
- /** Subscribe to ALL events */
415
- onAny(handler) {
416
- this.globalHandlers.add(handler);
417
- }
418
- /** Unsubscribe from a specific event type */
419
- off(type, handler) {
420
- this.handlers.get(type)?.delete(handler);
421
- }
422
- /** Unsubscribe from ALL events */
423
- offAny(handler) {
424
- this.globalHandlers.delete(handler);
425
- }
426
- /** Remove all listeners */
427
- clear() {
428
- this.handlers.clear();
429
- this.globalHandlers.clear();
430
- }
431
- };
432
- orgBus = new StateBus();
433
- }
434
- });
435
-
436
- // src/lib/session-registry.ts
437
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync4 } from "fs";
438
- import path5 from "path";
439
- import os5 from "os";
440
360
  function registerSession(entry) {
441
- const dir = path5.dirname(REGISTRY_PATH);
361
+ const dir = path4.dirname(REGISTRY_PATH);
442
362
  if (!existsSync4(dir)) {
443
- mkdirSync(dir, { recursive: true });
363
+ mkdirSync2(dir, { recursive: true });
444
364
  }
445
365
  const sessions = listSessions();
446
366
  const idx = sessions.findIndex((s) => s.windowName === entry.windowName);
@@ -453,7 +373,7 @@ function registerSession(entry) {
453
373
  }
454
374
  function listSessions() {
455
375
  try {
456
- const raw = readFileSync4(REGISTRY_PATH, "utf8");
376
+ const raw = readFileSync3(REGISTRY_PATH, "utf8");
457
377
  return JSON.parse(raw);
458
378
  } catch {
459
379
  return [];
@@ -463,7 +383,7 @@ var REGISTRY_PATH;
463
383
  var init_session_registry = __esm({
464
384
  "src/lib/session-registry.ts"() {
465
385
  "use strict";
466
- REGISTRY_PATH = path5.join(os5.homedir(), ".exe-os", "session-registry.json");
386
+ REGISTRY_PATH = path4.join(os4.homedir(), ".exe-os", "session-registry.json");
467
387
  }
468
388
  });
469
389
 
@@ -743,12 +663,12 @@ var init_runtime_table = __esm({
743
663
  });
744
664
 
745
665
  // src/lib/agent-config.ts
746
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
747
- import path6 from "path";
666
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
667
+ import path5 from "path";
748
668
  function loadAgentConfig() {
749
669
  if (!existsSync5(AGENT_CONFIG_PATH)) return {};
750
670
  try {
751
- return JSON.parse(readFileSync5(AGENT_CONFIG_PATH, "utf-8"));
671
+ return JSON.parse(readFileSync4(AGENT_CONFIG_PATH, "utf-8"));
752
672
  } catch {
753
673
  return {};
754
674
  }
@@ -767,7 +687,8 @@ var init_agent_config = __esm({
767
687
  "use strict";
768
688
  init_config();
769
689
  init_runtime_table();
770
- AGENT_CONFIG_PATH = path6.join(EXE_AI_DIR, "agent-config.json");
690
+ init_secure_files();
691
+ AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
771
692
  DEFAULT_MODELS = {
772
693
  claude: "claude-opus-4",
773
694
  codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
@@ -785,17 +706,17 @@ __export(intercom_queue_exports, {
785
706
  queueIntercom: () => queueIntercom,
786
707
  readQueue: () => readQueue
787
708
  });
788
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
789
- import path7 from "path";
790
- import os6 from "os";
709
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
710
+ import path6 from "path";
711
+ import os5 from "os";
791
712
  function ensureDir() {
792
- const dir = path7.dirname(QUEUE_PATH);
713
+ const dir = path6.dirname(QUEUE_PATH);
793
714
  if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
794
715
  }
795
716
  function readQueue() {
796
717
  try {
797
718
  if (!existsSync6(QUEUE_PATH)) return [];
798
- return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
719
+ return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
799
720
  } catch {
800
721
  return [];
801
722
  }
@@ -895,26 +816,29 @@ var QUEUE_PATH, MAX_RETRIES, TTL_MS, INTERCOM_LOG;
895
816
  var init_intercom_queue = __esm({
896
817
  "src/lib/intercom-queue.ts"() {
897
818
  "use strict";
898
- QUEUE_PATH = path7.join(os6.homedir(), ".exe-os", "intercom-queue.json");
819
+ QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
899
820
  MAX_RETRIES = 5;
900
821
  TTL_MS = 60 * 60 * 1e3;
901
- INTERCOM_LOG = path7.join(os6.homedir(), ".exe-os", "intercom.log");
822
+ INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
902
823
  }
903
824
  });
904
825
 
905
826
  // src/lib/license.ts
906
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
827
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
907
828
  import { randomUUID } from "crypto";
908
- import path8 from "path";
829
+ import { createRequire as createRequire2 } from "module";
830
+ import { pathToFileURL as pathToFileURL2 } from "url";
831
+ import os6 from "os";
832
+ import path7 from "path";
909
833
  import { jwtVerify, importSPKI } from "jose";
910
834
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
911
835
  var init_license = __esm({
912
836
  "src/lib/license.ts"() {
913
837
  "use strict";
914
838
  init_config();
915
- LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
916
- CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
917
- DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
839
+ LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
840
+ CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
841
+ DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
918
842
  PLAN_LIMITS = {
919
843
  free: { devices: 1, employees: 1, memories: 5e3 },
920
844
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -926,12 +850,12 @@ var init_license = __esm({
926
850
  });
927
851
 
928
852
  // src/lib/plan-limits.ts
929
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
930
- import path9 from "path";
853
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
854
+ import path8 from "path";
931
855
  function getLicenseSync() {
932
856
  try {
933
857
  if (!existsSync8(CACHE_PATH2)) return freeLicense();
934
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
858
+ const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
935
859
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
936
860
  const parts = raw.token.split(".");
937
861
  if (parts.length !== 3) return freeLicense();
@@ -970,7 +894,7 @@ function assertEmployeeLimitSync(rosterPath) {
970
894
  let count = 0;
971
895
  try {
972
896
  if (existsSync8(filePath)) {
973
- const raw = readFileSync8(filePath, "utf8");
897
+ const raw = readFileSync7(filePath, "utf8");
974
898
  const employees = JSON.parse(raw);
975
899
  count = Array.isArray(employees) ? employees.length : 0;
976
900
  }
@@ -999,12 +923,12 @@ var init_plan_limits = __esm({
999
923
  this.name = "PlanLimitError";
1000
924
  }
1001
925
  };
1002
- CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
926
+ CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
1003
927
  }
1004
928
  });
1005
929
 
1006
930
  // src/lib/session-kill-telemetry.ts
1007
- import crypto2 from "crypto";
931
+ import crypto from "crypto";
1008
932
  async function recordSessionKill(input) {
1009
933
  try {
1010
934
  const client = getClient();
@@ -1014,7 +938,7 @@ async function recordSessionKill(input) {
1014
938
  ticks_idle, estimated_tokens_saved)
1015
939
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
1016
940
  args: [
1017
- crypto2.randomUUID(),
941
+ crypto.randomUUID(),
1018
942
  input.sessionName,
1019
943
  input.agentId,
1020
944
  (/* @__PURE__ */ new Date()).toISOString(),
@@ -1337,6 +1261,7 @@ __export(tmux_routing_exports, {
1337
1261
  isEmployeeAlive: () => isEmployeeAlive,
1338
1262
  isExeSession: () => isExeSession,
1339
1263
  isSessionBusy: () => isSessionBusy,
1264
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
1340
1265
  notifyParentExe: () => notifyParentExe,
1341
1266
  parseParentExe: () => parseParentExe,
1342
1267
  registerParentExe: () => registerParentExe,
@@ -1347,13 +1272,13 @@ __export(tmux_routing_exports, {
1347
1272
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
1348
1273
  });
1349
1274
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
1350
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync as readdirSync2 } from "fs";
1351
- import path10 from "path";
1275
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync, readdirSync } from "fs";
1276
+ import path9 from "path";
1352
1277
  import os7 from "os";
1353
1278
  import { fileURLToPath } from "url";
1354
- import { unlinkSync as unlinkSync3 } from "fs";
1279
+ import { unlinkSync as unlinkSync2 } from "fs";
1355
1280
  function spawnLockPath(sessionName) {
1356
- return path10.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1281
+ return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1357
1282
  }
1358
1283
  function isProcessAlive(pid) {
1359
1284
  try {
@@ -1370,7 +1295,7 @@ function acquireSpawnLock(sessionName) {
1370
1295
  const lockFile = spawnLockPath(sessionName);
1371
1296
  if (existsSync9(lockFile)) {
1372
1297
  try {
1373
- const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
1298
+ const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1374
1299
  const age = Date.now() - lock.timestamp;
1375
1300
  if (isProcessAlive(lock.pid) && age < 6e4) {
1376
1301
  return false;
@@ -1383,15 +1308,15 @@ function acquireSpawnLock(sessionName) {
1383
1308
  }
1384
1309
  function releaseSpawnLock(sessionName) {
1385
1310
  try {
1386
- unlinkSync3(spawnLockPath(sessionName));
1311
+ unlinkSync2(spawnLockPath(sessionName));
1387
1312
  } catch {
1388
1313
  }
1389
1314
  }
1390
1315
  function resolveBehaviorsExporterScript() {
1391
1316
  try {
1392
1317
  const thisFile = fileURLToPath(import.meta.url);
1393
- const scriptPath = path10.join(
1394
- path10.dirname(thisFile),
1318
+ const scriptPath = path9.join(
1319
+ path9.dirname(thisFile),
1395
1320
  "..",
1396
1321
  "bin",
1397
1322
  "exe-export-behaviors.js"
@@ -1466,7 +1391,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1466
1391
  mkdirSync5(SESSION_CACHE, { recursive: true });
1467
1392
  }
1468
1393
  const rootExe = extractRootExe(parentExe) ?? parentExe;
1469
- const filePath = path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1394
+ const filePath = path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1470
1395
  writeFileSync6(filePath, JSON.stringify({
1471
1396
  parentExe: rootExe,
1472
1397
  dispatchedBy: dispatchedBy || rootExe,
@@ -1475,7 +1400,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1475
1400
  }
1476
1401
  function getParentExe(sessionKey) {
1477
1402
  try {
1478
- const data = JSON.parse(readFileSync9(path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1403
+ const data = JSON.parse(readFileSync8(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1479
1404
  return data.parentExe || null;
1480
1405
  } catch {
1481
1406
  return null;
@@ -1483,8 +1408,8 @@ function getParentExe(sessionKey) {
1483
1408
  }
1484
1409
  function getDispatchedBy(sessionKey) {
1485
1410
  try {
1486
- const data = JSON.parse(readFileSync9(
1487
- path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1411
+ const data = JSON.parse(readFileSync8(
1412
+ path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1488
1413
  "utf8"
1489
1414
  ));
1490
1415
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -1555,7 +1480,7 @@ async function verifyPaneAtCapacity(sessionName) {
1555
1480
  function readDebounceState() {
1556
1481
  try {
1557
1482
  if (!existsSync9(DEBOUNCE_FILE)) return {};
1558
- const raw = JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
1483
+ const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
1559
1484
  const state = {};
1560
1485
  for (const [key, val] of Object.entries(raw)) {
1561
1486
  if (typeof val === "number") {
@@ -1670,7 +1595,7 @@ function sendIntercom(targetSession) {
1670
1595
  try {
1671
1596
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
1672
1597
  const agent = baseAgentName(rawAgent);
1673
- const markerPath = path10.join(SESSION_CACHE, `current-task-${agent}.json`);
1598
+ const markerPath = path9.join(SESSION_CACHE, `current-task-${agent}.json`);
1674
1599
  if (existsSync9(markerPath)) {
1675
1600
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
1676
1601
  return "debounced";
@@ -1680,9 +1605,9 @@ function sendIntercom(targetSession) {
1680
1605
  try {
1681
1606
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
1682
1607
  const agent = baseAgentName(rawAgent);
1683
- const taskDir = path10.join(process.cwd(), "exe", agent);
1608
+ const taskDir = path9.join(process.cwd(), "exe", agent);
1684
1609
  if (existsSync9(taskDir)) {
1685
- const files = readdirSync2(taskDir).filter(
1610
+ const files = readdirSync(taskDir).filter(
1686
1611
  (f) => f.endsWith(".md") && f !== "DONE.txt"
1687
1612
  );
1688
1613
  if (files.length === 0) {
@@ -1741,6 +1666,21 @@ function notifyParentExe(sessionKey) {
1741
1666
  }
1742
1667
  return true;
1743
1668
  }
1669
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
1670
+ const transport = getTransport();
1671
+ try {
1672
+ const sessions = transport.listSessions();
1673
+ if (!sessions.includes(coordinatorSession)) return false;
1674
+ execSync4(
1675
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
1676
+ { timeout: 3e3 }
1677
+ );
1678
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
1679
+ return true;
1680
+ } catch {
1681
+ return false;
1682
+ }
1683
+ }
1744
1684
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1745
1685
  if (isCoordinatorName(employeeName)) {
1746
1686
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -1814,8 +1754,8 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1814
1754
  const transport = getTransport();
1815
1755
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
1816
1756
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
1817
- const logDir = path10.join(os7.homedir(), ".exe-os", "session-logs");
1818
- const logFile = path10.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1757
+ const logDir = path9.join(os7.homedir(), ".exe-os", "session-logs");
1758
+ const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1819
1759
  if (!existsSync9(logDir)) {
1820
1760
  mkdirSync5(logDir, { recursive: true });
1821
1761
  }
@@ -1823,17 +1763,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1823
1763
  let cleanupSuffix = "";
1824
1764
  try {
1825
1765
  const thisFile = fileURLToPath(import.meta.url);
1826
- const cleanupScript = path10.join(path10.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1766
+ const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1827
1767
  if (existsSync9(cleanupScript)) {
1828
1768
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
1829
1769
  }
1830
1770
  } catch {
1831
1771
  }
1832
1772
  try {
1833
- const claudeJsonPath = path10.join(os7.homedir(), ".claude.json");
1773
+ const claudeJsonPath = path9.join(os7.homedir(), ".claude.json");
1834
1774
  let claudeJson = {};
1835
1775
  try {
1836
- claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
1776
+ claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
1837
1777
  } catch {
1838
1778
  }
1839
1779
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -1845,13 +1785,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1845
1785
  } catch {
1846
1786
  }
1847
1787
  try {
1848
- const settingsDir = path10.join(os7.homedir(), ".claude", "projects");
1788
+ const settingsDir = path9.join(os7.homedir(), ".claude", "projects");
1849
1789
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
1850
- const projSettingsDir = path10.join(settingsDir, normalizedKey);
1851
- const settingsPath = path10.join(projSettingsDir, "settings.json");
1790
+ const projSettingsDir = path9.join(settingsDir, normalizedKey);
1791
+ const settingsPath = path9.join(projSettingsDir, "settings.json");
1852
1792
  let settings = {};
1853
1793
  try {
1854
- settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
1794
+ settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
1855
1795
  } catch {
1856
1796
  }
1857
1797
  const perms = settings.permissions ?? {};
@@ -1895,7 +1835,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1895
1835
  let behaviorsFlag = "";
1896
1836
  let legacyFallbackWarned = false;
1897
1837
  if (!useExeAgent && !useBinSymlink) {
1898
- const identityPath = path10.join(
1838
+ const identityPath = path9.join(
1899
1839
  os7.homedir(),
1900
1840
  ".exe-os",
1901
1841
  "identity",
@@ -1911,7 +1851,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1911
1851
  }
1912
1852
  const behaviorsFile = exportBehaviorsSync(
1913
1853
  employeeName,
1914
- path10.basename(spawnCwd),
1854
+ path9.basename(spawnCwd),
1915
1855
  sessionName
1916
1856
  );
1917
1857
  if (behaviorsFile) {
@@ -1926,9 +1866,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1926
1866
  }
1927
1867
  let sessionContextFlag = "";
1928
1868
  try {
1929
- const ctxDir = path10.join(os7.homedir(), ".exe-os", "session-cache");
1869
+ const ctxDir = path9.join(os7.homedir(), ".exe-os", "session-cache");
1930
1870
  mkdirSync5(ctxDir, { recursive: true });
1931
- const ctxFile = path10.join(ctxDir, `session-context-${sessionName}.md`);
1871
+ const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
1932
1872
  const ctxContent = [
1933
1873
  `## Session Context`,
1934
1874
  `You are running in tmux session: ${sessionName}.`,
@@ -2012,7 +1952,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
2012
1952
  transport.pipeLog(sessionName, logFile);
2013
1953
  try {
2014
1954
  const mySession = getMySession();
2015
- const dispatchInfo = path10.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
1955
+ const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
2016
1956
  writeFileSync6(dispatchInfo, JSON.stringify({
2017
1957
  dispatchedBy: mySession,
2018
1958
  rootExe: exeSession,
@@ -2087,15 +2027,15 @@ var init_tmux_routing = __esm({
2087
2027
  init_intercom_queue();
2088
2028
  init_plan_limits();
2089
2029
  init_employees();
2090
- SPAWN_LOCK_DIR = path10.join(os7.homedir(), ".exe-os", "spawn-locks");
2091
- SESSION_CACHE = path10.join(os7.homedir(), ".exe-os", "session-cache");
2030
+ SPAWN_LOCK_DIR = path9.join(os7.homedir(), ".exe-os", "spawn-locks");
2031
+ SESSION_CACHE = path9.join(os7.homedir(), ".exe-os", "session-cache");
2092
2032
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
2093
2033
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
2094
2034
  VERIFY_PANE_LINES = 200;
2095
2035
  INTERCOM_DEBOUNCE_MS = 3e4;
2096
2036
  CODEX_DEBOUNCE_MS = 12e4;
2097
- INTERCOM_LOG2 = path10.join(os7.homedir(), ".exe-os", "intercom.log");
2098
- DEBOUNCE_FILE = path10.join(SESSION_CACHE, "intercom-debounce.json");
2037
+ INTERCOM_LOG2 = path9.join(os7.homedir(), ".exe-os", "intercom.log");
2038
+ DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
2099
2039
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
2100
2040
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
2101
2041
  }
@@ -2118,6 +2058,15 @@ function sessionScopeFilter(sessionScope, tableAlias) {
2118
2058
  args: [scope]
2119
2059
  };
2120
2060
  }
2061
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
2062
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
2063
+ if (!scope) return { sql: "", args: [] };
2064
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
2065
+ return {
2066
+ sql: ` AND ${col} = ?`,
2067
+ args: [scope]
2068
+ };
2069
+ }
2121
2070
  var init_task_scope = __esm({
2122
2071
  "src/lib/task-scope.ts"() {
2123
2072
  "use strict";
@@ -2125,13 +2074,229 @@ var init_task_scope = __esm({
2125
2074
  }
2126
2075
  });
2127
2076
 
2128
- // src/lib/tasks-crud.ts
2129
- import crypto3 from "crypto";
2130
- import path11 from "path";
2077
+ // src/lib/notifications.ts
2078
+ import crypto2 from "crypto";
2079
+ import path10 from "path";
2131
2080
  import os8 from "os";
2081
+ import {
2082
+ readFileSync as readFileSync9,
2083
+ readdirSync as readdirSync2,
2084
+ unlinkSync as unlinkSync3,
2085
+ existsSync as existsSync10,
2086
+ rmdirSync
2087
+ } from "fs";
2088
+ async function writeNotification(notification) {
2089
+ try {
2090
+ const client = getClient();
2091
+ const id = crypto2.randomUUID();
2092
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2093
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
2094
+ await client.execute({
2095
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
2096
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
2097
+ args: [
2098
+ id,
2099
+ notification.agentId,
2100
+ notification.agentRole,
2101
+ notification.event,
2102
+ notification.project,
2103
+ notification.summary,
2104
+ notification.taskFile ?? null,
2105
+ sessionScope,
2106
+ now
2107
+ ]
2108
+ });
2109
+ } catch (err) {
2110
+ process.stderr.write(`[notifications] WRITE FAILED: ${err instanceof Error ? err.message : String(err)}
2111
+ `);
2112
+ }
2113
+ }
2114
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
2115
+ try {
2116
+ const client = getClient();
2117
+ const scope = strictSessionScopeFilter(sessionScope);
2118
+ await client.execute({
2119
+ sql: `UPDATE notifications SET read = 1
2120
+ WHERE task_file = ? AND read = 0${scope.sql}`,
2121
+ args: [taskFile, ...scope.args]
2122
+ });
2123
+ } catch {
2124
+ }
2125
+ }
2126
+ var init_notifications = __esm({
2127
+ "src/lib/notifications.ts"() {
2128
+ "use strict";
2129
+ init_database();
2130
+ init_task_scope();
2131
+ }
2132
+ });
2133
+
2134
+ // src/lib/state-bus.ts
2135
+ var StateBus, orgBus;
2136
+ var init_state_bus = __esm({
2137
+ "src/lib/state-bus.ts"() {
2138
+ "use strict";
2139
+ StateBus = class {
2140
+ handlers = /* @__PURE__ */ new Map();
2141
+ globalHandlers = /* @__PURE__ */ new Set();
2142
+ /** Emit an event to all subscribers */
2143
+ emit(event) {
2144
+ const typeHandlers = this.handlers.get(event.type);
2145
+ if (typeHandlers) {
2146
+ for (const handler of typeHandlers) {
2147
+ try {
2148
+ handler(event);
2149
+ } catch {
2150
+ }
2151
+ }
2152
+ }
2153
+ for (const handler of this.globalHandlers) {
2154
+ try {
2155
+ handler(event);
2156
+ } catch {
2157
+ }
2158
+ }
2159
+ }
2160
+ /** Subscribe to a specific event type */
2161
+ on(type, handler) {
2162
+ if (!this.handlers.has(type)) {
2163
+ this.handlers.set(type, /* @__PURE__ */ new Set());
2164
+ }
2165
+ this.handlers.get(type).add(handler);
2166
+ }
2167
+ /** Subscribe to ALL events */
2168
+ onAny(handler) {
2169
+ this.globalHandlers.add(handler);
2170
+ }
2171
+ /** Unsubscribe from a specific event type */
2172
+ off(type, handler) {
2173
+ this.handlers.get(type)?.delete(handler);
2174
+ }
2175
+ /** Unsubscribe from ALL events */
2176
+ offAny(handler) {
2177
+ this.globalHandlers.delete(handler);
2178
+ }
2179
+ /** Remove all listeners */
2180
+ clear() {
2181
+ this.handlers.clear();
2182
+ this.globalHandlers.clear();
2183
+ }
2184
+ };
2185
+ orgBus = new StateBus();
2186
+ }
2187
+ });
2188
+
2189
+ // src/lib/project-name.ts
2132
2190
  import { execSync as execSync5 } from "child_process";
2191
+ import path11 from "path";
2192
+ function getProjectName(cwd) {
2193
+ const dir = cwd ?? process.cwd();
2194
+ if (_cached2 && _cachedCwd === dir) return _cached2;
2195
+ try {
2196
+ let repoRoot;
2197
+ try {
2198
+ const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
2199
+ cwd: dir,
2200
+ encoding: "utf8",
2201
+ timeout: 2e3,
2202
+ stdio: ["pipe", "pipe", "pipe"]
2203
+ }).trim();
2204
+ repoRoot = path11.dirname(gitCommonDir);
2205
+ } catch {
2206
+ repoRoot = execSync5("git rev-parse --show-toplevel", {
2207
+ cwd: dir,
2208
+ encoding: "utf8",
2209
+ timeout: 2e3,
2210
+ stdio: ["pipe", "pipe", "pipe"]
2211
+ }).trim();
2212
+ }
2213
+ _cached2 = path11.basename(repoRoot);
2214
+ _cachedCwd = dir;
2215
+ return _cached2;
2216
+ } catch {
2217
+ _cached2 = path11.basename(dir);
2218
+ _cachedCwd = dir;
2219
+ return _cached2;
2220
+ }
2221
+ }
2222
+ var _cached2, _cachedCwd;
2223
+ var init_project_name = __esm({
2224
+ "src/lib/project-name.ts"() {
2225
+ "use strict";
2226
+ _cached2 = null;
2227
+ _cachedCwd = null;
2228
+ }
2229
+ });
2230
+
2231
+ // src/lib/session-scope.ts
2232
+ var session_scope_exports = {};
2233
+ __export(session_scope_exports, {
2234
+ assertSessionScope: () => assertSessionScope,
2235
+ findSessionForProject: () => findSessionForProject,
2236
+ getSessionProject: () => getSessionProject
2237
+ });
2238
+ function getSessionProject(sessionName) {
2239
+ const sessions = listSessions();
2240
+ const entry = sessions.find((s) => s.windowName === sessionName);
2241
+ if (!entry) return null;
2242
+ const parts = entry.projectDir.split("/").filter(Boolean);
2243
+ return parts[parts.length - 1] ?? null;
2244
+ }
2245
+ function findSessionForProject(projectName) {
2246
+ const sessions = listSessions();
2247
+ for (const s of sessions) {
2248
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
2249
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
2250
+ }
2251
+ return null;
2252
+ }
2253
+ function assertSessionScope(actionType, targetProject) {
2254
+ try {
2255
+ const currentProject = getProjectName();
2256
+ const exeSession = resolveExeSession();
2257
+ if (!exeSession) {
2258
+ return { allowed: true, reason: "no_session" };
2259
+ }
2260
+ if (currentProject === targetProject) {
2261
+ return {
2262
+ allowed: true,
2263
+ reason: "same_session",
2264
+ currentProject,
2265
+ targetProject
2266
+ };
2267
+ }
2268
+ process.stderr.write(
2269
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
2270
+ `
2271
+ );
2272
+ return {
2273
+ allowed: false,
2274
+ reason: "cross_session_denied",
2275
+ currentProject,
2276
+ targetProject,
2277
+ targetSession: findSessionForProject(targetProject)?.windowName
2278
+ };
2279
+ } catch {
2280
+ return { allowed: true, reason: "no_session" };
2281
+ }
2282
+ }
2283
+ var init_session_scope = __esm({
2284
+ "src/lib/session-scope.ts"() {
2285
+ "use strict";
2286
+ init_session_registry();
2287
+ init_project_name();
2288
+ init_tmux_routing();
2289
+ init_employees();
2290
+ }
2291
+ });
2292
+
2293
+ // src/lib/tasks-crud.ts
2294
+ import crypto3 from "crypto";
2295
+ import path12 from "path";
2296
+ import os9 from "os";
2297
+ import { execSync as execSync6 } from "child_process";
2133
2298
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2134
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
2299
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2135
2300
  async function writeCheckpoint(input) {
2136
2301
  const client = getClient();
2137
2302
  const row = await resolveTask(client, input.taskId);
@@ -2251,9 +2416,24 @@ async function createTaskCore(input) {
2251
2416
  const now = (/* @__PURE__ */ new Date()).toISOString();
2252
2417
  const slug = slugify(input.title);
2253
2418
  let earlySessionScope = null;
2419
+ let scopeMismatchWarning;
2254
2420
  try {
2255
2421
  const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2256
- earlySessionScope = resolveExeSession2();
2422
+ const resolved = resolveExeSession2();
2423
+ if (resolved && input.projectName) {
2424
+ const { getSessionProject: getSessionProject2 } = await Promise.resolve().then(() => (init_session_scope(), session_scope_exports));
2425
+ const sessionProject = getSessionProject2(resolved);
2426
+ if (sessionProject && sessionProject !== input.projectName) {
2427
+ scopeMismatchWarning = `session/project mismatch: session "${resolved}" owns "${sessionProject}" but task targets "${input.projectName}". Routed to default scope.`;
2428
+ process.stderr.write(`[create_task] ${scopeMismatchWarning}
2429
+ `);
2430
+ earlySessionScope = null;
2431
+ } else {
2432
+ earlySessionScope = resolved;
2433
+ }
2434
+ } else {
2435
+ earlySessionScope = resolved;
2436
+ }
2257
2437
  } catch {
2258
2438
  }
2259
2439
  const scope = earlySessionScope ?? "default";
@@ -2304,10 +2484,14 @@ async function createTaskCore(input) {
2304
2484
  ${laneWarning}` : laneWarning;
2305
2485
  }
2306
2486
  }
2487
+ if (scopeMismatchWarning) {
2488
+ warning = warning ? `${warning}
2489
+ ${scopeMismatchWarning}` : scopeMismatchWarning;
2490
+ }
2307
2491
  if (input.baseDir) {
2308
2492
  try {
2309
- await mkdir3(path11.join(input.baseDir, "exe", "output"), { recursive: true });
2310
- await mkdir3(path11.join(input.baseDir, "exe", "research"), { recursive: true });
2493
+ await mkdir3(path12.join(input.baseDir, "exe", "output"), { recursive: true });
2494
+ await mkdir3(path12.join(input.baseDir, "exe", "research"), { recursive: true });
2311
2495
  await ensureArchitectureDoc(input.baseDir, input.projectName);
2312
2496
  await ensureGitignoreExe(input.baseDir);
2313
2497
  } catch {
@@ -2343,13 +2527,19 @@ ${laneWarning}` : laneWarning;
2343
2527
  });
2344
2528
  if (input.baseDir) {
2345
2529
  try {
2346
- const EXE_OS_DIR = path11.join(os8.homedir(), ".exe-os");
2347
- const mdPath = path11.join(EXE_OS_DIR, taskFile);
2348
- const mdDir = path11.dirname(mdPath);
2349
- if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2530
+ const EXE_OS_DIR = path12.join(os9.homedir(), ".exe-os");
2531
+ const mdPath = path12.join(EXE_OS_DIR, taskFile);
2532
+ const mdDir = path12.dirname(mdPath);
2533
+ if (!existsSync11(mdDir)) await mkdir3(mdDir, { recursive: true });
2350
2534
  const reviewer = input.reviewer ?? input.assignedBy;
2351
2535
  const mdContent = `# ${input.title}
2352
2536
 
2537
+ ## MANDATORY: When done
2538
+
2539
+ You MUST call update_task with status "done" and a result summary when finished.
2540
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2541
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2542
+
2353
2543
  **ID:** ${id}
2354
2544
  **Status:** ${initialStatus}
2355
2545
  **Priority:** ${input.priority}
@@ -2363,12 +2553,6 @@ ${laneWarning}` : laneWarning;
2363
2553
  ## Context
2364
2554
 
2365
2555
  ${input.context}
2366
-
2367
- ## MANDATORY: When done
2368
-
2369
- You MUST call update_task with status "done" and a result summary when finished.
2370
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2371
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
2372
2556
  `;
2373
2557
  await writeFile3(mdPath, mdContent, "utf-8");
2374
2558
  } catch (err) {
@@ -2450,14 +2634,14 @@ function isTmuxSessionAlive(identifier) {
2450
2634
  if (!identifier || identifier === "unknown") return true;
2451
2635
  try {
2452
2636
  if (identifier.startsWith("%")) {
2453
- const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
2637
+ const output = execSync6("tmux list-panes -a -F '#{pane_id}'", {
2454
2638
  timeout: 2e3,
2455
2639
  encoding: "utf8",
2456
2640
  stdio: ["pipe", "pipe", "pipe"]
2457
2641
  });
2458
2642
  return output.split("\n").some((l) => l.trim() === identifier);
2459
2643
  } else {
2460
- execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2644
+ execSync6(`tmux has-session -t ${JSON.stringify(identifier)}`, {
2461
2645
  timeout: 2e3,
2462
2646
  stdio: ["pipe", "pipe", "pipe"]
2463
2647
  });
@@ -2466,7 +2650,7 @@ function isTmuxSessionAlive(identifier) {
2466
2650
  } catch {
2467
2651
  if (identifier.startsWith("%")) return true;
2468
2652
  try {
2469
- execSync5("tmux list-sessions", {
2653
+ execSync6("tmux list-sessions", {
2470
2654
  timeout: 2e3,
2471
2655
  stdio: ["pipe", "pipe", "pipe"]
2472
2656
  });
@@ -2481,12 +2665,12 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
2481
2665
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
2482
2666
  try {
2483
2667
  const since = new Date(taskCreatedAt).toISOString();
2484
- const branch = execSync5(
2668
+ const branch = execSync6(
2485
2669
  "git rev-parse --abbrev-ref HEAD 2>/dev/null",
2486
2670
  { encoding: "utf8", timeout: 3e3 }
2487
2671
  ).trim();
2488
2672
  const branchArg = branch && branch !== "HEAD" ? branch : "";
2489
- const commitCount = execSync5(
2673
+ const commitCount = execSync6(
2490
2674
  `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
2491
2675
  { encoding: "utf8", timeout: 5e3 }
2492
2676
  ).trim();
@@ -2617,7 +2801,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2617
2801
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
2618
2802
  } catch {
2619
2803
  }
2620
- if (input.status === "done" || input.status === "cancelled") {
2804
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
2621
2805
  try {
2622
2806
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
2623
2807
  clearQueueForAgent2(String(row.assigned_to));
@@ -2646,9 +2830,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2646
2830
  return { taskFile, assignedTo, assignedBy, taskSlug };
2647
2831
  }
2648
2832
  async function ensureArchitectureDoc(baseDir, projectName) {
2649
- const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
2833
+ const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
2650
2834
  try {
2651
- if (existsSync10(archPath)) return;
2835
+ if (existsSync11(archPath)) return;
2652
2836
  const template = [
2653
2837
  `# ${projectName} \u2014 System Architecture`,
2654
2838
  "",
@@ -2681,9 +2865,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2681
2865
  }
2682
2866
  }
2683
2867
  async function ensureGitignoreExe(baseDir) {
2684
- const gitignorePath = path11.join(baseDir, ".gitignore");
2868
+ const gitignorePath = path12.join(baseDir, ".gitignore");
2685
2869
  try {
2686
- if (existsSync10(gitignorePath)) {
2870
+ if (existsSync11(gitignorePath)) {
2687
2871
  const content = readFileSync10(gitignorePath, "utf-8");
2688
2872
  if (/^\/?exe\/?$/m.test(content)) return;
2689
2873
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -2715,58 +2899,42 @@ var init_tasks_crud = __esm({
2715
2899
  });
2716
2900
 
2717
2901
  // src/lib/tasks-review.ts
2718
- import path12 from "path";
2719
- import { existsSync as existsSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
2902
+ import path13 from "path";
2903
+ import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
2720
2904
  async function countPendingReviews(sessionScope) {
2721
2905
  const client = getClient();
2722
- if (sessionScope) {
2723
- const result2 = await client.execute({
2724
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND session_scope = ?",
2725
- args: [sessionScope]
2726
- });
2727
- return Number(result2.rows[0]?.cnt) || 0;
2728
- }
2906
+ const scope = strictSessionScopeFilter(
2907
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2908
+ );
2729
2909
  const result = await client.execute({
2730
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
2731
- args: []
2910
+ sql: `SELECT COUNT(*) as cnt FROM tasks
2911
+ WHERE status = 'needs_review'${scope.sql}`,
2912
+ args: [...scope.args]
2732
2913
  });
2733
2914
  return Number(result.rows[0]?.cnt) || 0;
2734
2915
  }
2735
2916
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2736
2917
  const client = getClient();
2737
- if (sessionScope) {
2738
- const result2 = await client.execute({
2739
- sql: `SELECT COUNT(*) as cnt FROM tasks
2740
- WHERE status = 'needs_review' AND updated_at > ?
2741
- AND session_scope = ?`,
2742
- args: [sinceIso, sessionScope]
2743
- });
2744
- return Number(result2.rows[0]?.cnt) || 0;
2745
- }
2918
+ const scope = strictSessionScopeFilter(
2919
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2920
+ );
2746
2921
  const result = await client.execute({
2747
2922
  sql: `SELECT COUNT(*) as cnt FROM tasks
2748
- WHERE status = 'needs_review' AND updated_at > ?`,
2749
- args: [sinceIso]
2923
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
2924
+ args: [sinceIso, ...scope.args]
2750
2925
  });
2751
2926
  return Number(result.rows[0]?.cnt) || 0;
2752
2927
  }
2753
2928
  async function listPendingReviews(limit, sessionScope) {
2754
2929
  const client = getClient();
2755
- if (sessionScope) {
2756
- const result2 = await client.execute({
2757
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
2758
- WHERE status = 'needs_review'
2759
- AND session_scope = ?
2760
- ORDER BY updated_at ASC LIMIT ?`,
2761
- args: [sessionScope, limit]
2762
- });
2763
- return result2.rows;
2764
- }
2930
+ const scope = strictSessionScopeFilter(
2931
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2932
+ );
2765
2933
  const result = await client.execute({
2766
2934
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
2767
- WHERE status = 'needs_review'
2935
+ WHERE status = 'needs_review'${scope.sql}
2768
2936
  ORDER BY updated_at ASC LIMIT ?`,
2769
- args: [limit]
2937
+ args: [...scope.args, limit]
2770
2938
  });
2771
2939
  return result.rows;
2772
2940
  }
@@ -2778,7 +2946,7 @@ async function cleanupOrphanedReviews() {
2778
2946
  WHERE status IN ('open', 'needs_review', 'in_progress')
2779
2947
  AND assigned_by = 'system'
2780
2948
  AND title LIKE 'Review:%'
2781
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
2949
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
2782
2950
  args: [now]
2783
2951
  });
2784
2952
  const r1b = await client.execute({
@@ -2897,11 +3065,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2897
3065
  );
2898
3066
  }
2899
3067
  try {
2900
- const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
2901
- if (existsSync11(cacheDir)) {
3068
+ const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3069
+ if (existsSync12(cacheDir)) {
2902
3070
  for (const f of readdirSync3(cacheDir)) {
2903
3071
  if (f.startsWith("review-notified-")) {
2904
- unlinkSync4(path12.join(cacheDir, f));
3072
+ unlinkSync4(path13.join(cacheDir, f));
2905
3073
  }
2906
3074
  }
2907
3075
  }
@@ -2918,11 +3086,12 @@ var init_tasks_review = __esm({
2918
3086
  init_tmux_routing();
2919
3087
  init_session_key();
2920
3088
  init_state_bus();
3089
+ init_task_scope();
2921
3090
  }
2922
3091
  });
2923
3092
 
2924
3093
  // src/lib/tasks-chain.ts
2925
- import path13 from "path";
3094
+ import path14 from "path";
2926
3095
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2927
3096
  async function cascadeUnblock(taskId, baseDir, now) {
2928
3097
  const client = getClient();
@@ -2939,7 +3108,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
2939
3108
  });
2940
3109
  for (const ur of unblockedRows.rows) {
2941
3110
  try {
2942
- const ubFile = path13.join(baseDir, String(ur.task_file));
3111
+ const ubFile = path14.join(baseDir, String(ur.task_file));
2943
3112
  let ubContent = await readFile3(ubFile, "utf-8");
2944
3113
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
2945
3114
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -2974,7 +3143,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
2974
3143
  const scScope = sessionScopeFilter();
2975
3144
  const remaining = await client.execute({
2976
3145
  sql: `SELECT COUNT(*) as cnt FROM tasks
2977
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
3146
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
2978
3147
  args: [parentTaskId, ...scScope.args]
2979
3148
  });
2980
3149
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -3006,110 +3175,6 @@ var init_tasks_chain = __esm({
3006
3175
  }
3007
3176
  });
3008
3177
 
3009
- // src/lib/project-name.ts
3010
- import { execSync as execSync6 } from "child_process";
3011
- import path14 from "path";
3012
- function getProjectName(cwd) {
3013
- const dir = cwd ?? process.cwd();
3014
- if (_cached2 && _cachedCwd === dir) return _cached2;
3015
- try {
3016
- let repoRoot;
3017
- try {
3018
- const gitCommonDir = execSync6("git rev-parse --path-format=absolute --git-common-dir", {
3019
- cwd: dir,
3020
- encoding: "utf8",
3021
- timeout: 2e3,
3022
- stdio: ["pipe", "pipe", "pipe"]
3023
- }).trim();
3024
- repoRoot = path14.dirname(gitCommonDir);
3025
- } catch {
3026
- repoRoot = execSync6("git rev-parse --show-toplevel", {
3027
- cwd: dir,
3028
- encoding: "utf8",
3029
- timeout: 2e3,
3030
- stdio: ["pipe", "pipe", "pipe"]
3031
- }).trim();
3032
- }
3033
- _cached2 = path14.basename(repoRoot);
3034
- _cachedCwd = dir;
3035
- return _cached2;
3036
- } catch {
3037
- _cached2 = path14.basename(dir);
3038
- _cachedCwd = dir;
3039
- return _cached2;
3040
- }
3041
- }
3042
- var _cached2, _cachedCwd;
3043
- var init_project_name = __esm({
3044
- "src/lib/project-name.ts"() {
3045
- "use strict";
3046
- _cached2 = null;
3047
- _cachedCwd = null;
3048
- }
3049
- });
3050
-
3051
- // src/lib/session-scope.ts
3052
- var session_scope_exports = {};
3053
- __export(session_scope_exports, {
3054
- assertSessionScope: () => assertSessionScope,
3055
- findSessionForProject: () => findSessionForProject,
3056
- getSessionProject: () => getSessionProject
3057
- });
3058
- function getSessionProject(sessionName) {
3059
- const sessions = listSessions();
3060
- const entry = sessions.find((s) => s.windowName === sessionName);
3061
- if (!entry) return null;
3062
- const parts = entry.projectDir.split("/").filter(Boolean);
3063
- return parts[parts.length - 1] ?? null;
3064
- }
3065
- function findSessionForProject(projectName) {
3066
- const sessions = listSessions();
3067
- for (const s of sessions) {
3068
- const proj = s.projectDir.split("/").filter(Boolean).pop();
3069
- if (proj === projectName && isCoordinatorName(s.agentId)) return s;
3070
- }
3071
- return null;
3072
- }
3073
- function assertSessionScope(actionType, targetProject) {
3074
- try {
3075
- const currentProject = getProjectName();
3076
- const exeSession = resolveExeSession();
3077
- if (!exeSession) {
3078
- return { allowed: true, reason: "no_session" };
3079
- }
3080
- if (currentProject === targetProject) {
3081
- return {
3082
- allowed: true,
3083
- reason: "same_session",
3084
- currentProject,
3085
- targetProject
3086
- };
3087
- }
3088
- process.stderr.write(
3089
- `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
3090
- `
3091
- );
3092
- return {
3093
- allowed: false,
3094
- reason: "cross_session_denied",
3095
- currentProject,
3096
- targetProject,
3097
- targetSession: findSessionForProject(targetProject)?.windowName
3098
- };
3099
- } catch {
3100
- return { allowed: true, reason: "no_session" };
3101
- }
3102
- }
3103
- var init_session_scope = __esm({
3104
- "src/lib/session-scope.ts"() {
3105
- "use strict";
3106
- init_session_registry();
3107
- init_project_name();
3108
- init_tmux_routing();
3109
- init_employees();
3110
- }
3111
- });
3112
-
3113
3178
  // src/lib/tasks-notify.ts
3114
3179
  async function dispatchTaskToEmployee(input) {
3115
3180
  if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
@@ -3532,7 +3597,7 @@ async function updateTask(input) {
3532
3597
  if (input.status === "in_progress") {
3533
3598
  mkdirSync6(cacheDir, { recursive: true });
3534
3599
  writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3535
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3600
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
3536
3601
  try {
3537
3602
  unlinkSync5(cachePath);
3538
3603
  } catch {
@@ -3540,10 +3605,10 @@ async function updateTask(input) {
3540
3605
  }
3541
3606
  } catch {
3542
3607
  }
3543
- if (input.status === "done") {
3608
+ if (input.status === "done" || input.status === "closed") {
3544
3609
  await cleanupReviewFile(row, taskFile, input.baseDir);
3545
3610
  }
3546
- if (input.status === "done" || input.status === "cancelled") {
3611
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
3547
3612
  try {
3548
3613
  const client = getClient();
3549
3614
  const taskTitle = String(row.title);
@@ -3559,7 +3624,7 @@ async function updateTask(input) {
3559
3624
  if (!isCoordinatorName(assignedAgent)) {
3560
3625
  try {
3561
3626
  const draftClient = getClient();
3562
- if (input.status === "done") {
3627
+ if (input.status === "done" || input.status === "closed") {
3563
3628
  await draftClient.execute({
3564
3629
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3565
3630
  args: [assignedAgent]
@@ -3576,7 +3641,7 @@ async function updateTask(input) {
3576
3641
  try {
3577
3642
  const client = getClient();
3578
3643
  const cascaded = await client.execute({
3579
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
3644
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
3580
3645
  WHERE parent_task_id = ? AND status = 'needs_review'`,
3581
3646
  args: [now, taskId]
3582
3647
  });
@@ -3589,14 +3654,14 @@ async function updateTask(input) {
3589
3654
  } catch {
3590
3655
  }
3591
3656
  }
3592
- const isTerminal = input.status === "done" || input.status === "needs_review";
3657
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
3593
3658
  if (isTerminal) {
3594
3659
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
3595
3660
  if (!isCoordinator) {
3596
3661
  notifyTaskDone();
3597
3662
  }
3598
3663
  await markTaskNotificationsRead(taskFile);
3599
- if (input.status === "done") {
3664
+ if (input.status === "done" || input.status === "closed") {
3600
3665
  try {
3601
3666
  await cascadeUnblock(taskId, input.baseDir, now);
3602
3667
  } catch {
@@ -3616,7 +3681,7 @@ async function updateTask(input) {
3616
3681
  }
3617
3682
  }
3618
3683
  }
3619
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3684
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3620
3685
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3621
3686
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3622
3687
  taskId,