@askexenow/exe-os 0.9.8 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/bin/backfill-conversations.js +222 -49
  2. package/dist/bin/backfill-responses.js +221 -48
  3. package/dist/bin/backfill-vectors.js +225 -52
  4. package/dist/bin/cleanup-stale-review-tasks.js +150 -28
  5. package/dist/bin/cli.js +1295 -856
  6. package/dist/bin/exe-agent-config.js +36 -8
  7. package/dist/bin/exe-agent.js +14 -4
  8. package/dist/bin/exe-assign.js +221 -48
  9. package/dist/bin/exe-boot.js +778 -427
  10. package/dist/bin/exe-call.js +41 -13
  11. package/dist/bin/exe-cloud.js +163 -58
  12. package/dist/bin/exe-dispatch.js +276 -139
  13. package/dist/bin/exe-doctor.js +145 -27
  14. package/dist/bin/exe-export-behaviors.js +141 -23
  15. package/dist/bin/exe-forget.js +137 -19
  16. package/dist/bin/exe-gateway.js +677 -388
  17. package/dist/bin/exe-heartbeat.js +227 -108
  18. package/dist/bin/exe-kill.js +138 -20
  19. package/dist/bin/exe-launch-agent.js +172 -39
  20. package/dist/bin/exe-link.js +291 -100
  21. package/dist/bin/exe-new-employee.js +214 -106
  22. package/dist/bin/exe-pending-messages.js +395 -33
  23. package/dist/bin/exe-pending-notifications.js +684 -99
  24. package/dist/bin/exe-pending-reviews.js +420 -74
  25. package/dist/bin/exe-rename.js +147 -49
  26. package/dist/bin/exe-review.js +138 -20
  27. package/dist/bin/exe-search.js +240 -69
  28. package/dist/bin/exe-session-cleanup.js +440 -250
  29. package/dist/bin/exe-settings.js +61 -17
  30. package/dist/bin/exe-start-codex.js +158 -39
  31. package/dist/bin/exe-start-opencode.js +157 -38
  32. package/dist/bin/exe-status.js +151 -29
  33. package/dist/bin/exe-team.js +138 -20
  34. package/dist/bin/git-sweep.js +404 -212
  35. package/dist/bin/graph-backfill.js +137 -19
  36. package/dist/bin/graph-export.js +140 -22
  37. package/dist/bin/install.js +90 -61
  38. package/dist/bin/scan-tasks.js +412 -220
  39. package/dist/bin/setup.js +564 -293
  40. package/dist/bin/shard-migrate.js +139 -21
  41. package/dist/bin/update.js +138 -49
  42. package/dist/bin/wiki-sync.js +137 -19
  43. package/dist/gateway/index.js +533 -320
  44. package/dist/hooks/bug-report-worker.js +344 -193
  45. package/dist/hooks/codex-stop-task-finalizer.js +4678 -0
  46. package/dist/hooks/commit-complete.js +402 -210
  47. package/dist/hooks/error-recall.js +245 -74
  48. package/dist/hooks/exe-heartbeat-hook.js +16 -6
  49. package/dist/hooks/ingest-worker.js +3423 -3157
  50. package/dist/hooks/ingest.js +832 -97
  51. package/dist/hooks/instructions-loaded.js +227 -54
  52. package/dist/hooks/notification.js +216 -43
  53. package/dist/hooks/post-compact.js +239 -62
  54. package/dist/hooks/pre-compact.js +408 -216
  55. package/dist/hooks/pre-tool-use.js +268 -90
  56. package/dist/hooks/prompt-ingest-worker.js +352 -102
  57. package/dist/hooks/prompt-submit.js +541 -328
  58. package/dist/hooks/response-ingest-worker.js +372 -122
  59. package/dist/hooks/session-end.js +443 -240
  60. package/dist/hooks/session-start.js +313 -127
  61. package/dist/hooks/stop.js +293 -98
  62. package/dist/hooks/subagent-stop.js +239 -62
  63. package/dist/hooks/summary-worker.js +568 -236
  64. package/dist/index.js +538 -324
  65. package/dist/lib/agent-config.js +28 -6
  66. package/dist/lib/cloud-sync.js +284 -105
  67. package/dist/lib/config.js +30 -10
  68. package/dist/lib/consolidation.js +16 -6
  69. package/dist/lib/database.js +123 -25
  70. package/dist/lib/db-daemon-client.js +73 -19
  71. package/dist/lib/db.js +123 -25
  72. package/dist/lib/device-registry.js +133 -35
  73. package/dist/lib/embedder.js +107 -32
  74. package/dist/lib/employee-templates.js +14 -4
  75. package/dist/lib/employees.js +41 -13
  76. package/dist/lib/exe-daemon-client.js +88 -22
  77. package/dist/lib/exe-daemon.js +935 -587
  78. package/dist/lib/hybrid-search.js +240 -69
  79. package/dist/lib/identity.js +18 -8
  80. package/dist/lib/license.js +133 -48
  81. package/dist/lib/messaging.js +116 -56
  82. package/dist/lib/reminders.js +14 -4
  83. package/dist/lib/schedules.js +137 -19
  84. package/dist/lib/skill-learning.js +33 -6
  85. package/dist/lib/store.js +137 -19
  86. package/dist/lib/task-router.js +14 -4
  87. package/dist/lib/tasks.js +280 -234
  88. package/dist/lib/tmux-routing.js +172 -125
  89. package/dist/lib/token-spend.js +26 -8
  90. package/dist/mcp/server.js +1326 -609
  91. package/dist/mcp/tools/complete-reminder.js +14 -4
  92. package/dist/mcp/tools/create-reminder.js +14 -4
  93. package/dist/mcp/tools/create-task.js +306 -248
  94. package/dist/mcp/tools/deactivate-behavior.js +16 -6
  95. package/dist/mcp/tools/list-reminders.js +14 -4
  96. package/dist/mcp/tools/list-tasks.js +123 -107
  97. package/dist/mcp/tools/send-message.js +75 -29
  98. package/dist/mcp/tools/update-task.js +1848 -199
  99. package/dist/runtime/index.js +441 -248
  100. package/dist/tui/App.js +761 -424
  101. package/package.json +1 -1
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,125 @@ var init_task_scope = __esm({
2125
2074
  }
2126
2075
  });
2127
2076
 
2077
+ // src/lib/notifications.ts
2078
+ import crypto2 from "crypto";
2079
+ import path10 from "path";
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
+
2128
2189
  // src/lib/tasks-crud.ts
2129
2190
  import crypto3 from "crypto";
2130
2191
  import path11 from "path";
2131
- import os8 from "os";
2192
+ import os9 from "os";
2132
2193
  import { execSync as execSync5 } from "child_process";
2133
2194
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
2134
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
2195
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
2135
2196
  async function writeCheckpoint(input) {
2136
2197
  const client = getClient();
2137
2198
  const row = await resolveTask(client, input.taskId);
@@ -2343,13 +2404,19 @@ ${laneWarning}` : laneWarning;
2343
2404
  });
2344
2405
  if (input.baseDir) {
2345
2406
  try {
2346
- const EXE_OS_DIR = path11.join(os8.homedir(), ".exe-os");
2407
+ const EXE_OS_DIR = path11.join(os9.homedir(), ".exe-os");
2347
2408
  const mdPath = path11.join(EXE_OS_DIR, taskFile);
2348
2409
  const mdDir = path11.dirname(mdPath);
2349
- if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2410
+ if (!existsSync11(mdDir)) await mkdir3(mdDir, { recursive: true });
2350
2411
  const reviewer = input.reviewer ?? input.assignedBy;
2351
2412
  const mdContent = `# ${input.title}
2352
2413
 
2414
+ ## MANDATORY: When done
2415
+
2416
+ You MUST call update_task with status "done" and a result summary when finished.
2417
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2418
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2419
+
2353
2420
  **ID:** ${id}
2354
2421
  **Status:** ${initialStatus}
2355
2422
  **Priority:** ${input.priority}
@@ -2363,12 +2430,6 @@ ${laneWarning}` : laneWarning;
2363
2430
  ## Context
2364
2431
 
2365
2432
  ${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
2433
  `;
2373
2434
  await writeFile3(mdPath, mdContent, "utf-8");
2374
2435
  } catch (err) {
@@ -2617,7 +2678,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2617
2678
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
2618
2679
  } catch {
2619
2680
  }
2620
- if (input.status === "done" || input.status === "cancelled") {
2681
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
2621
2682
  try {
2622
2683
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
2623
2684
  clearQueueForAgent2(String(row.assigned_to));
@@ -2648,7 +2709,7 @@ async function deleteTaskCore(taskId, _baseDir) {
2648
2709
  async function ensureArchitectureDoc(baseDir, projectName) {
2649
2710
  const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
2650
2711
  try {
2651
- if (existsSync10(archPath)) return;
2712
+ if (existsSync11(archPath)) return;
2652
2713
  const template = [
2653
2714
  `# ${projectName} \u2014 System Architecture`,
2654
2715
  "",
@@ -2683,7 +2744,7 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2683
2744
  async function ensureGitignoreExe(baseDir) {
2684
2745
  const gitignorePath = path11.join(baseDir, ".gitignore");
2685
2746
  try {
2686
- if (existsSync10(gitignorePath)) {
2747
+ if (existsSync11(gitignorePath)) {
2687
2748
  const content = readFileSync10(gitignorePath, "utf-8");
2688
2749
  if (/^\/?exe\/?$/m.test(content)) return;
2689
2750
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -2716,57 +2777,41 @@ var init_tasks_crud = __esm({
2716
2777
 
2717
2778
  // src/lib/tasks-review.ts
2718
2779
  import path12 from "path";
2719
- import { existsSync as existsSync11, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
2780
+ import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
2720
2781
  async function countPendingReviews(sessionScope) {
2721
2782
  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
- }
2783
+ const scope = strictSessionScopeFilter(
2784
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2785
+ );
2729
2786
  const result = await client.execute({
2730
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
2731
- args: []
2787
+ sql: `SELECT COUNT(*) as cnt FROM tasks
2788
+ WHERE status = 'needs_review'${scope.sql}`,
2789
+ args: [...scope.args]
2732
2790
  });
2733
2791
  return Number(result.rows[0]?.cnt) || 0;
2734
2792
  }
2735
2793
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2736
2794
  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
- }
2795
+ const scope = strictSessionScopeFilter(
2796
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2797
+ );
2746
2798
  const result = await client.execute({
2747
2799
  sql: `SELECT COUNT(*) as cnt FROM tasks
2748
- WHERE status = 'needs_review' AND updated_at > ?`,
2749
- args: [sinceIso]
2800
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
2801
+ args: [sinceIso, ...scope.args]
2750
2802
  });
2751
2803
  return Number(result.rows[0]?.cnt) || 0;
2752
2804
  }
2753
2805
  async function listPendingReviews(limit, sessionScope) {
2754
2806
  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
- }
2807
+ const scope = strictSessionScopeFilter(
2808
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
2809
+ );
2765
2810
  const result = await client.execute({
2766
2811
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
2767
- WHERE status = 'needs_review'
2812
+ WHERE status = 'needs_review'${scope.sql}
2768
2813
  ORDER BY updated_at ASC LIMIT ?`,
2769
- args: [limit]
2814
+ args: [...scope.args, limit]
2770
2815
  });
2771
2816
  return result.rows;
2772
2817
  }
@@ -2778,7 +2823,7 @@ async function cleanupOrphanedReviews() {
2778
2823
  WHERE status IN ('open', 'needs_review', 'in_progress')
2779
2824
  AND assigned_by = 'system'
2780
2825
  AND title LIKE 'Review:%'
2781
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
2826
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
2782
2827
  args: [now]
2783
2828
  });
2784
2829
  const r1b = await client.execute({
@@ -2898,7 +2943,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2898
2943
  }
2899
2944
  try {
2900
2945
  const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
2901
- if (existsSync11(cacheDir)) {
2946
+ if (existsSync12(cacheDir)) {
2902
2947
  for (const f of readdirSync3(cacheDir)) {
2903
2948
  if (f.startsWith("review-notified-")) {
2904
2949
  unlinkSync4(path12.join(cacheDir, f));
@@ -2918,6 +2963,7 @@ var init_tasks_review = __esm({
2918
2963
  init_tmux_routing();
2919
2964
  init_session_key();
2920
2965
  init_state_bus();
2966
+ init_task_scope();
2921
2967
  }
2922
2968
  });
2923
2969
 
@@ -2974,7 +3020,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
2974
3020
  const scScope = sessionScopeFilter();
2975
3021
  const remaining = await client.execute({
2976
3022
  sql: `SELECT COUNT(*) as cnt FROM tasks
2977
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
3023
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
2978
3024
  args: [parentTaskId, ...scScope.args]
2979
3025
  });
2980
3026
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -3532,7 +3578,7 @@ async function updateTask(input) {
3532
3578
  if (input.status === "in_progress") {
3533
3579
  mkdirSync6(cacheDir, { recursive: true });
3534
3580
  writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3535
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3581
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
3536
3582
  try {
3537
3583
  unlinkSync5(cachePath);
3538
3584
  } catch {
@@ -3540,10 +3586,10 @@ async function updateTask(input) {
3540
3586
  }
3541
3587
  } catch {
3542
3588
  }
3543
- if (input.status === "done") {
3589
+ if (input.status === "done" || input.status === "closed") {
3544
3590
  await cleanupReviewFile(row, taskFile, input.baseDir);
3545
3591
  }
3546
- if (input.status === "done" || input.status === "cancelled") {
3592
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
3547
3593
  try {
3548
3594
  const client = getClient();
3549
3595
  const taskTitle = String(row.title);
@@ -3559,7 +3605,7 @@ async function updateTask(input) {
3559
3605
  if (!isCoordinatorName(assignedAgent)) {
3560
3606
  try {
3561
3607
  const draftClient = getClient();
3562
- if (input.status === "done") {
3608
+ if (input.status === "done" || input.status === "closed") {
3563
3609
  await draftClient.execute({
3564
3610
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
3565
3611
  args: [assignedAgent]
@@ -3576,7 +3622,7 @@ async function updateTask(input) {
3576
3622
  try {
3577
3623
  const client = getClient();
3578
3624
  const cascaded = await client.execute({
3579
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
3625
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
3580
3626
  WHERE parent_task_id = ? AND status = 'needs_review'`,
3581
3627
  args: [now, taskId]
3582
3628
  });
@@ -3589,14 +3635,14 @@ async function updateTask(input) {
3589
3635
  } catch {
3590
3636
  }
3591
3637
  }
3592
- const isTerminal = input.status === "done" || input.status === "needs_review";
3638
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
3593
3639
  if (isTerminal) {
3594
3640
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
3595
3641
  if (!isCoordinator) {
3596
3642
  notifyTaskDone();
3597
3643
  }
3598
3644
  await markTaskNotificationsRead(taskFile);
3599
- if (input.status === "done") {
3645
+ if (input.status === "done" || input.status === "closed") {
3600
3646
  try {
3601
3647
  await cascadeUnblock(taskId, input.baseDir, now);
3602
3648
  } catch {
@@ -3616,7 +3662,7 @@ async function updateTask(input) {
3616
3662
  }
3617
3663
  }
3618
3664
  }
3619
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3665
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3620
3666
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3621
3667
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3622
3668
  taskId,