@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
@@ -307,9 +307,34 @@ var init_provider_table = __esm({
307
307
  }
308
308
  });
309
309
 
310
+ // src/lib/secure-files.ts
311
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
312
+ import { chmod, mkdir } from "fs/promises";
313
+ async function ensurePrivateDir(dirPath) {
314
+ await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
315
+ try {
316
+ await chmod(dirPath, PRIVATE_DIR_MODE);
317
+ } catch {
318
+ }
319
+ }
320
+ async function enforcePrivateFile(filePath) {
321
+ try {
322
+ await chmod(filePath, PRIVATE_FILE_MODE);
323
+ } catch {
324
+ }
325
+ }
326
+ var PRIVATE_DIR_MODE, PRIVATE_FILE_MODE;
327
+ var init_secure_files = __esm({
328
+ "src/lib/secure-files.ts"() {
329
+ "use strict";
330
+ PRIVATE_DIR_MODE = 448;
331
+ PRIVATE_FILE_MODE = 384;
332
+ }
333
+ });
334
+
310
335
  // src/lib/config.ts
311
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
312
- import { readFileSync as readFileSync2, existsSync as existsSync2, renameSync } from "fs";
336
+ import { readFile, writeFile } from "fs/promises";
337
+ import { readFileSync as readFileSync2, existsSync as existsSync3, renameSync } from "fs";
313
338
  import path2 from "path";
314
339
  import os2 from "os";
315
340
  function resolveDataDir() {
@@ -317,7 +342,7 @@ function resolveDataDir() {
317
342
  if (process.env.EXE_MEM_DIR) return process.env.EXE_MEM_DIR;
318
343
  const newDir = path2.join(os2.homedir(), ".exe-os");
319
344
  const legacyDir = path2.join(os2.homedir(), ".exe-mem");
320
- if (!existsSync2(newDir) && existsSync2(legacyDir)) {
345
+ if (!existsSync3(newDir) && existsSync3(legacyDir)) {
321
346
  try {
322
347
  renameSync(legacyDir, newDir);
323
348
  process.stderr.write(`[exe-os] Migrated data directory: ~/.exe-mem \u2192 ~/.exe-os
@@ -380,9 +405,9 @@ function normalizeAutoUpdate(raw) {
380
405
  }
381
406
  async function loadConfig() {
382
407
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
383
- await mkdir(dir, { recursive: true });
408
+ await ensurePrivateDir(dir);
384
409
  const configPath = path2.join(dir, "config.json");
385
- if (!existsSync2(configPath)) {
410
+ if (!existsSync3(configPath)) {
386
411
  return { ...DEFAULT_CONFIG, dbPath: path2.join(dir, "memories.db") };
387
412
  }
388
413
  const raw = await readFile(configPath, "utf-8");
@@ -395,6 +420,7 @@ async function loadConfig() {
395
420
  `);
396
421
  try {
397
422
  await writeFile(configPath, JSON.stringify(migratedCfg, null, 2) + "\n");
423
+ await enforcePrivateFile(configPath);
398
424
  } catch {
399
425
  }
400
426
  }
@@ -414,6 +440,7 @@ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CON
414
440
  var init_config = __esm({
415
441
  "src/lib/config.ts"() {
416
442
  "use strict";
443
+ init_secure_files();
417
444
  EXE_AI_DIR = resolveDataDir();
418
445
  DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
419
446
  MODELS_DIR = path2.join(EXE_AI_DIR, "models");
@@ -518,10 +545,10 @@ var init_runtime_table = __esm({
518
545
  });
519
546
 
520
547
  // src/lib/agent-config.ts
521
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
548
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
522
549
  import path3 from "path";
523
550
  function loadAgentConfig() {
524
- if (!existsSync3(AGENT_CONFIG_PATH)) return {};
551
+ if (!existsSync4(AGENT_CONFIG_PATH)) return {};
525
552
  try {
526
553
  return JSON.parse(readFileSync3(AGENT_CONFIG_PATH, "utf-8"));
527
554
  } catch {
@@ -542,6 +569,7 @@ var init_agent_config = __esm({
542
569
  "use strict";
543
570
  init_config();
544
571
  init_runtime_table();
572
+ init_secure_files();
545
573
  AGENT_CONFIG_PATH = path3.join(EXE_AI_DIR, "agent-config.json");
546
574
  DEFAULT_MODELS = {
547
575
  claude: "claude-opus-4",
@@ -560,16 +588,16 @@ __export(intercom_queue_exports, {
560
588
  queueIntercom: () => queueIntercom,
561
589
  readQueue: () => readQueue
562
590
  });
563
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
591
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, renameSync as renameSync2, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
564
592
  import path4 from "path";
565
593
  import os3 from "os";
566
594
  function ensureDir() {
567
595
  const dir = path4.dirname(QUEUE_PATH);
568
- if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
596
+ if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
569
597
  }
570
598
  function readQueue() {
571
599
  try {
572
- if (!existsSync4(QUEUE_PATH)) return [];
600
+ if (!existsSync5(QUEUE_PATH)) return [];
573
601
  return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
574
602
  } catch {
575
603
  return [];
@@ -686,7 +714,7 @@ var init_db_retry = __esm({
686
714
 
687
715
  // src/lib/employees.ts
688
716
  import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
689
- import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
717
+ import { existsSync as existsSync6, symlinkSync, readlinkSync, readFileSync as readFileSync5, renameSync as renameSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
690
718
  import { execSync as execSync3 } from "child_process";
691
719
  import path5 from "path";
692
720
  import os4 from "os";
@@ -707,7 +735,7 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
707
735
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
708
736
  }
709
737
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
710
- if (!existsSync5(employeesPath)) return [];
738
+ if (!existsSync6(employeesPath)) return [];
711
739
  try {
712
740
  return JSON.parse(readFileSync5(employeesPath, "utf-8"));
713
741
  } catch {
@@ -796,8 +824,11 @@ var init_database = __esm({
796
824
  });
797
825
 
798
826
  // src/lib/license.ts
799
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
827
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
800
828
  import { randomUUID } from "crypto";
829
+ import { createRequire as createRequire2 } from "module";
830
+ import { pathToFileURL as pathToFileURL2 } from "url";
831
+ import os6 from "os";
801
832
  import path7 from "path";
802
833
  import { jwtVerify, importSPKI } from "jose";
803
834
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
@@ -819,11 +850,11 @@ var init_license = __esm({
819
850
  });
820
851
 
821
852
  // src/lib/plan-limits.ts
822
- import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
853
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
823
854
  import path8 from "path";
824
855
  function getLicenseSync() {
825
856
  try {
826
- if (!existsSync7(CACHE_PATH2)) return freeLicense();
857
+ if (!existsSync8(CACHE_PATH2)) return freeLicense();
827
858
  const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
828
859
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
829
860
  const parts = raw.token.split(".");
@@ -862,7 +893,7 @@ function assertEmployeeLimitSync(rosterPath) {
862
893
  const filePath = rosterPath ?? EMPLOYEES_PATH;
863
894
  let count = 0;
864
895
  try {
865
- if (existsSync7(filePath)) {
896
+ if (existsSync8(filePath)) {
866
897
  const raw = readFileSync7(filePath, "utf8");
867
898
  const employees = JSON.parse(raw);
868
899
  count = Array.isArray(employees) ? employees.length : 0;
@@ -896,15 +927,48 @@ var init_plan_limits = __esm({
896
927
  }
897
928
  });
898
929
 
930
+ // src/lib/task-scope.ts
931
+ function getCurrentSessionScope() {
932
+ try {
933
+ return resolveExeSession();
934
+ } catch {
935
+ return null;
936
+ }
937
+ }
938
+ function sessionScopeFilter(sessionScope, tableAlias) {
939
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
940
+ if (!scope) return { sql: "", args: [] };
941
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
942
+ return {
943
+ sql: ` AND (${col} IS NULL OR ${col} = ?)`,
944
+ args: [scope]
945
+ };
946
+ }
947
+ function strictSessionScopeFilter(sessionScope, tableAlias) {
948
+ const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
949
+ if (!scope) return { sql: "", args: [] };
950
+ const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
951
+ return {
952
+ sql: ` AND ${col} = ?`,
953
+ args: [scope]
954
+ };
955
+ }
956
+ var init_task_scope = __esm({
957
+ "src/lib/task-scope.ts"() {
958
+ "use strict";
959
+ init_tmux_routing();
960
+ }
961
+ });
962
+
899
963
  // src/lib/notifications.ts
900
964
  import crypto from "crypto";
901
965
  import path9 from "path";
902
- import os6 from "os";
966
+ import os7 from "os";
903
967
  import {
904
968
  readFileSync as readFileSync8,
905
969
  readdirSync,
906
970
  unlinkSync as unlinkSync2,
907
- existsSync as existsSync8,
971
+ existsSync as existsSync9,
908
972
  rmdirSync
909
973
  } from "fs";
910
974
  async function writeNotification(notification) {
@@ -912,9 +976,10 @@ async function writeNotification(notification) {
912
976
  const client = getClient();
913
977
  const id = crypto.randomUUID();
914
978
  const now = (/* @__PURE__ */ new Date()).toISOString();
979
+ const sessionScope = notification.sessionScope === void 0 ? getCurrentSessionScope() : notification.sessionScope;
915
980
  await client.execute({
916
- sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, read, created_at)
917
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?)`,
981
+ sql: `INSERT INTO notifications (id, agent_id, agent_role, event, project, summary, task_file, session_scope, read, created_at)
982
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`,
918
983
  args: [
919
984
  id,
920
985
  notification.agentId,
@@ -923,6 +988,7 @@ async function writeNotification(notification) {
923
988
  notification.project,
924
989
  notification.summary,
925
990
  notification.taskFile ?? null,
991
+ sessionScope,
926
992
  now
927
993
  ]
928
994
  });
@@ -931,12 +997,14 @@ async function writeNotification(notification) {
931
997
  `);
932
998
  }
933
999
  }
934
- async function markAsReadByTaskFile(taskFile) {
1000
+ async function markAsReadByTaskFile(taskFile, sessionScope) {
935
1001
  try {
936
1002
  const client = getClient();
1003
+ const scope = strictSessionScopeFilter(sessionScope);
937
1004
  await client.execute({
938
- sql: "UPDATE notifications SET read = 1 WHERE task_file = ? AND read = 0",
939
- args: [taskFile]
1005
+ sql: `UPDATE notifications SET read = 1
1006
+ WHERE task_file = ? AND read = 0${scope.sql}`,
1007
+ args: [taskFile, ...scope.args]
940
1008
  });
941
1009
  } catch {
942
1010
  }
@@ -945,6 +1013,7 @@ var init_notifications = __esm({
945
1013
  "src/lib/notifications.ts"() {
946
1014
  "use strict";
947
1015
  init_database();
1016
+ init_task_scope();
948
1017
  }
949
1018
  });
950
1019
 
@@ -982,30 +1051,6 @@ var init_session_kill_telemetry = __esm({
982
1051
  }
983
1052
  });
984
1053
 
985
- // src/lib/task-scope.ts
986
- function getCurrentSessionScope() {
987
- try {
988
- return resolveExeSession();
989
- } catch {
990
- return null;
991
- }
992
- }
993
- function sessionScopeFilter(sessionScope, tableAlias) {
994
- const scope = sessionScope !== void 0 ? sessionScope : getCurrentSessionScope();
995
- if (!scope) return { sql: "", args: [] };
996
- const col = tableAlias ? `${tableAlias}.session_scope` : "session_scope";
997
- return {
998
- sql: ` AND (${col} IS NULL OR ${col} = ?)`,
999
- args: [scope]
1000
- };
1001
- }
1002
- var init_task_scope = __esm({
1003
- "src/lib/task-scope.ts"() {
1004
- "use strict";
1005
- init_tmux_routing();
1006
- }
1007
- });
1008
-
1009
1054
  // src/lib/state-bus.ts
1010
1055
  var StateBus, orgBus;
1011
1056
  var init_state_bus = __esm({
@@ -1064,10 +1109,10 @@ var init_state_bus = __esm({
1064
1109
  // src/lib/tasks-crud.ts
1065
1110
  import crypto3 from "crypto";
1066
1111
  import path10 from "path";
1067
- import os7 from "os";
1112
+ import os8 from "os";
1068
1113
  import { execSync as execSync4 } from "child_process";
1069
1114
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1070
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1115
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
1071
1116
  async function writeCheckpoint(input) {
1072
1117
  const client = getClient();
1073
1118
  const row = await resolveTask(client, input.taskId);
@@ -1279,13 +1324,19 @@ ${laneWarning}` : laneWarning;
1279
1324
  });
1280
1325
  if (input.baseDir) {
1281
1326
  try {
1282
- const EXE_OS_DIR = path10.join(os7.homedir(), ".exe-os");
1327
+ const EXE_OS_DIR = path10.join(os8.homedir(), ".exe-os");
1283
1328
  const mdPath = path10.join(EXE_OS_DIR, taskFile);
1284
1329
  const mdDir = path10.dirname(mdPath);
1285
- if (!existsSync9(mdDir)) await mkdir3(mdDir, { recursive: true });
1330
+ if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
1286
1331
  const reviewer = input.reviewer ?? input.assignedBy;
1287
1332
  const mdContent = `# ${input.title}
1288
1333
 
1334
+ ## MANDATORY: When done
1335
+
1336
+ You MUST call update_task with status "done" and a result summary when finished.
1337
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
1338
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
1339
+
1289
1340
  **ID:** ${id}
1290
1341
  **Status:** ${initialStatus}
1291
1342
  **Priority:** ${input.priority}
@@ -1299,12 +1350,6 @@ ${laneWarning}` : laneWarning;
1299
1350
  ## Context
1300
1351
 
1301
1352
  ${input.context}
1302
-
1303
- ## MANDATORY: When done
1304
-
1305
- You MUST call update_task with status "done" and a result summary when finished.
1306
- If you skip this, your reviewer will not know you're done and your work won't be reviewed.
1307
- Do NOT let a failed commit or any error prevent you from calling update_task(done).
1308
1353
  `;
1309
1354
  await writeFile3(mdPath, mdContent, "utf-8");
1310
1355
  } catch (err) {
@@ -1553,7 +1598,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1553
1598
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
1554
1599
  } catch {
1555
1600
  }
1556
- if (input.status === "done" || input.status === "cancelled") {
1601
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
1557
1602
  try {
1558
1603
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
1559
1604
  clearQueueForAgent2(String(row.assigned_to));
@@ -1584,7 +1629,7 @@ async function deleteTaskCore(taskId, _baseDir) {
1584
1629
  async function ensureArchitectureDoc(baseDir, projectName) {
1585
1630
  const archPath = path10.join(baseDir, "exe", "ARCHITECTURE.md");
1586
1631
  try {
1587
- if (existsSync9(archPath)) return;
1632
+ if (existsSync10(archPath)) return;
1588
1633
  const template = [
1589
1634
  `# ${projectName} \u2014 System Architecture`,
1590
1635
  "",
@@ -1619,7 +1664,7 @@ async function ensureArchitectureDoc(baseDir, projectName) {
1619
1664
  async function ensureGitignoreExe(baseDir) {
1620
1665
  const gitignorePath = path10.join(baseDir, ".gitignore");
1621
1666
  try {
1622
- if (existsSync9(gitignorePath)) {
1667
+ if (existsSync10(gitignorePath)) {
1623
1668
  const content = readFileSync9(gitignorePath, "utf-8");
1624
1669
  if (/^\/?exe\/?$/m.test(content)) return;
1625
1670
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -1652,57 +1697,41 @@ var init_tasks_crud = __esm({
1652
1697
 
1653
1698
  // src/lib/tasks-review.ts
1654
1699
  import path11 from "path";
1655
- import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
1700
+ import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
1656
1701
  async function countPendingReviews(sessionScope) {
1657
1702
  const client = getClient();
1658
- if (sessionScope) {
1659
- const result2 = await client.execute({
1660
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review' AND session_scope = ?",
1661
- args: [sessionScope]
1662
- });
1663
- return Number(result2.rows[0]?.cnt) || 0;
1664
- }
1703
+ const scope = strictSessionScopeFilter(
1704
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1705
+ );
1665
1706
  const result = await client.execute({
1666
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
1667
- args: []
1707
+ sql: `SELECT COUNT(*) as cnt FROM tasks
1708
+ WHERE status = 'needs_review'${scope.sql}`,
1709
+ args: [...scope.args]
1668
1710
  });
1669
1711
  return Number(result.rows[0]?.cnt) || 0;
1670
1712
  }
1671
1713
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
1672
1714
  const client = getClient();
1673
- if (sessionScope) {
1674
- const result2 = await client.execute({
1675
- sql: `SELECT COUNT(*) as cnt FROM tasks
1676
- WHERE status = 'needs_review' AND updated_at > ?
1677
- AND session_scope = ?`,
1678
- args: [sinceIso, sessionScope]
1679
- });
1680
- return Number(result2.rows[0]?.cnt) || 0;
1681
- }
1715
+ const scope = strictSessionScopeFilter(
1716
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1717
+ );
1682
1718
  const result = await client.execute({
1683
1719
  sql: `SELECT COUNT(*) as cnt FROM tasks
1684
- WHERE status = 'needs_review' AND updated_at > ?`,
1685
- args: [sinceIso]
1720
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
1721
+ args: [sinceIso, ...scope.args]
1686
1722
  });
1687
1723
  return Number(result.rows[0]?.cnt) || 0;
1688
1724
  }
1689
1725
  async function listPendingReviews(limit, sessionScope) {
1690
1726
  const client = getClient();
1691
- if (sessionScope) {
1692
- const result2 = await client.execute({
1693
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
1694
- WHERE status = 'needs_review'
1695
- AND session_scope = ?
1696
- ORDER BY updated_at ASC LIMIT ?`,
1697
- args: [sessionScope, limit]
1698
- });
1699
- return result2.rows;
1700
- }
1727
+ const scope = strictSessionScopeFilter(
1728
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1729
+ );
1701
1730
  const result = await client.execute({
1702
1731
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
1703
- WHERE status = 'needs_review'
1732
+ WHERE status = 'needs_review'${scope.sql}
1704
1733
  ORDER BY updated_at ASC LIMIT ?`,
1705
- args: [limit]
1734
+ args: [...scope.args, limit]
1706
1735
  });
1707
1736
  return result.rows;
1708
1737
  }
@@ -1714,7 +1743,7 @@ async function cleanupOrphanedReviews() {
1714
1743
  WHERE status IN ('open', 'needs_review', 'in_progress')
1715
1744
  AND assigned_by = 'system'
1716
1745
  AND title LIKE 'Review:%'
1717
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
1746
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
1718
1747
  args: [now]
1719
1748
  });
1720
1749
  const r1b = await client.execute({
@@ -1834,7 +1863,7 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
1834
1863
  }
1835
1864
  try {
1836
1865
  const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
1837
- if (existsSync10(cacheDir)) {
1866
+ if (existsSync11(cacheDir)) {
1838
1867
  for (const f of readdirSync2(cacheDir)) {
1839
1868
  if (f.startsWith("review-notified-")) {
1840
1869
  unlinkSync3(path11.join(cacheDir, f));
@@ -1854,6 +1883,7 @@ var init_tasks_review = __esm({
1854
1883
  init_tmux_routing();
1855
1884
  init_session_key();
1856
1885
  init_state_bus();
1886
+ init_task_scope();
1857
1887
  }
1858
1888
  });
1859
1889
 
@@ -1910,7 +1940,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
1910
1940
  const scScope = sessionScopeFilter();
1911
1941
  const remaining = await client.execute({
1912
1942
  sql: `SELECT COUNT(*) as cnt FROM tasks
1913
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
1943
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
1914
1944
  args: [parentTaskId, ...scScope.args]
1915
1945
  });
1916
1946
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -2468,7 +2498,7 @@ async function updateTask(input) {
2468
2498
  if (input.status === "in_progress") {
2469
2499
  mkdirSync5(cacheDir, { recursive: true });
2470
2500
  writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
2471
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
2501
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
2472
2502
  try {
2473
2503
  unlinkSync4(cachePath);
2474
2504
  } catch {
@@ -2476,10 +2506,10 @@ async function updateTask(input) {
2476
2506
  }
2477
2507
  } catch {
2478
2508
  }
2479
- if (input.status === "done") {
2509
+ if (input.status === "done" || input.status === "closed") {
2480
2510
  await cleanupReviewFile(row, taskFile, input.baseDir);
2481
2511
  }
2482
- if (input.status === "done" || input.status === "cancelled") {
2512
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
2483
2513
  try {
2484
2514
  const client = getClient();
2485
2515
  const taskTitle = String(row.title);
@@ -2495,7 +2525,7 @@ async function updateTask(input) {
2495
2525
  if (!isCoordinatorName(assignedAgent)) {
2496
2526
  try {
2497
2527
  const draftClient = getClient();
2498
- if (input.status === "done") {
2528
+ if (input.status === "done" || input.status === "closed") {
2499
2529
  await draftClient.execute({
2500
2530
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
2501
2531
  args: [assignedAgent]
@@ -2512,7 +2542,7 @@ async function updateTask(input) {
2512
2542
  try {
2513
2543
  const client = getClient();
2514
2544
  const cascaded = await client.execute({
2515
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
2545
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
2516
2546
  WHERE parent_task_id = ? AND status = 'needs_review'`,
2517
2547
  args: [now, taskId]
2518
2548
  });
@@ -2525,14 +2555,14 @@ async function updateTask(input) {
2525
2555
  } catch {
2526
2556
  }
2527
2557
  }
2528
- const isTerminal = input.status === "done" || input.status === "needs_review";
2558
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
2529
2559
  if (isTerminal) {
2530
2560
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
2531
2561
  if (!isCoordinator) {
2532
2562
  notifyTaskDone();
2533
2563
  }
2534
2564
  await markTaskNotificationsRead(taskFile);
2535
- if (input.status === "done") {
2565
+ if (input.status === "done" || input.status === "closed") {
2536
2566
  try {
2537
2567
  await cascadeUnblock(taskId, input.baseDir, now);
2538
2568
  } catch {
@@ -2552,7 +2582,7 @@ async function updateTask(input) {
2552
2582
  }
2553
2583
  }
2554
2584
  }
2555
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
2585
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
2556
2586
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
2557
2587
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
2558
2588
  taskId,
@@ -2924,6 +2954,7 @@ __export(tmux_routing_exports, {
2924
2954
  isEmployeeAlive: () => isEmployeeAlive,
2925
2955
  isExeSession: () => isExeSession,
2926
2956
  isSessionBusy: () => isSessionBusy,
2957
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
2927
2958
  notifyParentExe: () => notifyParentExe,
2928
2959
  parseParentExe: () => parseParentExe,
2929
2960
  registerParentExe: () => registerParentExe,
@@ -2934,9 +2965,9 @@ __export(tmux_routing_exports, {
2934
2965
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
2935
2966
  });
2936
2967
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
2937
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync, readdirSync as readdirSync3 } from "fs";
2968
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync, readdirSync as readdirSync3 } from "fs";
2938
2969
  import path15 from "path";
2939
- import os8 from "os";
2970
+ import os9 from "os";
2940
2971
  import { fileURLToPath } from "url";
2941
2972
  import { unlinkSync as unlinkSync5 } from "fs";
2942
2973
  function spawnLockPath(sessionName) {
@@ -2951,11 +2982,11 @@ function isProcessAlive(pid) {
2951
2982
  }
2952
2983
  }
2953
2984
  function acquireSpawnLock(sessionName) {
2954
- if (!existsSync11(SPAWN_LOCK_DIR)) {
2985
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
2955
2986
  mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
2956
2987
  }
2957
2988
  const lockFile = spawnLockPath(sessionName);
2958
- if (existsSync11(lockFile)) {
2989
+ if (existsSync12(lockFile)) {
2959
2990
  try {
2960
2991
  const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
2961
2992
  const age = Date.now() - lock.timestamp;
@@ -2983,7 +3014,7 @@ function resolveBehaviorsExporterScript() {
2983
3014
  "bin",
2984
3015
  "exe-export-behaviors.js"
2985
3016
  );
2986
- return existsSync11(scriptPath) ? scriptPath : null;
3017
+ return existsSync12(scriptPath) ? scriptPath : null;
2987
3018
  } catch {
2988
3019
  return null;
2989
3020
  }
@@ -3049,7 +3080,7 @@ function extractRootExe(name) {
3049
3080
  return parts.length > 0 ? parts[parts.length - 1] : null;
3050
3081
  }
3051
3082
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3052
- if (!existsSync11(SESSION_CACHE)) {
3083
+ if (!existsSync12(SESSION_CACHE)) {
3053
3084
  mkdirSync6(SESSION_CACHE, { recursive: true });
3054
3085
  }
3055
3086
  const rootExe = extractRootExe(parentExe) ?? parentExe;
@@ -3141,7 +3172,7 @@ async function verifyPaneAtCapacity(sessionName) {
3141
3172
  }
3142
3173
  function readDebounceState() {
3143
3174
  try {
3144
- if (!existsSync11(DEBOUNCE_FILE)) return {};
3175
+ if (!existsSync12(DEBOUNCE_FILE)) return {};
3145
3176
  const raw = JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
3146
3177
  const state = {};
3147
3178
  for (const [key, val] of Object.entries(raw)) {
@@ -3158,7 +3189,7 @@ function readDebounceState() {
3158
3189
  }
3159
3190
  function writeDebounceState(state) {
3160
3191
  try {
3161
- if (!existsSync11(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3192
+ if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3162
3193
  writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
3163
3194
  } catch {
3164
3195
  }
@@ -3258,7 +3289,7 @@ function sendIntercom(targetSession) {
3258
3289
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
3259
3290
  const agent = baseAgentName(rawAgent);
3260
3291
  const markerPath = path15.join(SESSION_CACHE, `current-task-${agent}.json`);
3261
- if (existsSync11(markerPath)) {
3292
+ if (existsSync12(markerPath)) {
3262
3293
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
3263
3294
  return "debounced";
3264
3295
  }
@@ -3268,7 +3299,7 @@ function sendIntercom(targetSession) {
3268
3299
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
3269
3300
  const agent = baseAgentName(rawAgent);
3270
3301
  const taskDir = path15.join(process.cwd(), "exe", agent);
3271
- if (existsSync11(taskDir)) {
3302
+ if (existsSync12(taskDir)) {
3272
3303
  const files = readdirSync3(taskDir).filter(
3273
3304
  (f) => f.endsWith(".md") && f !== "DONE.txt"
3274
3305
  );
@@ -3328,6 +3359,21 @@ function notifyParentExe(sessionKey) {
3328
3359
  }
3329
3360
  return true;
3330
3361
  }
3362
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
3363
+ const transport = getTransport();
3364
+ try {
3365
+ const sessions = transport.listSessions();
3366
+ if (!sessions.includes(coordinatorSession)) return false;
3367
+ execSync6(
3368
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
3369
+ { timeout: 3e3 }
3370
+ );
3371
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
3372
+ return true;
3373
+ } catch {
3374
+ return false;
3375
+ }
3376
+ }
3331
3377
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3332
3378
  if (isCoordinatorName(employeeName)) {
3333
3379
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -3401,9 +3447,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3401
3447
  const transport = getTransport();
3402
3448
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3403
3449
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3404
- const logDir = path15.join(os8.homedir(), ".exe-os", "session-logs");
3450
+ const logDir = path15.join(os9.homedir(), ".exe-os", "session-logs");
3405
3451
  const logFile = path15.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3406
- if (!existsSync11(logDir)) {
3452
+ if (!existsSync12(logDir)) {
3407
3453
  mkdirSync6(logDir, { recursive: true });
3408
3454
  }
3409
3455
  transport.kill(sessionName);
@@ -3411,13 +3457,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3411
3457
  try {
3412
3458
  const thisFile = fileURLToPath(import.meta.url);
3413
3459
  const cleanupScript = path15.join(path15.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3414
- if (existsSync11(cleanupScript)) {
3460
+ if (existsSync12(cleanupScript)) {
3415
3461
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
3416
3462
  }
3417
3463
  } catch {
3418
3464
  }
3419
3465
  try {
3420
- const claudeJsonPath = path15.join(os8.homedir(), ".claude.json");
3466
+ const claudeJsonPath = path15.join(os9.homedir(), ".claude.json");
3421
3467
  let claudeJson = {};
3422
3468
  try {
3423
3469
  claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
@@ -3432,7 +3478,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3432
3478
  } catch {
3433
3479
  }
3434
3480
  try {
3435
- const settingsDir = path15.join(os8.homedir(), ".claude", "projects");
3481
+ const settingsDir = path15.join(os9.homedir(), ".claude", "projects");
3436
3482
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3437
3483
  const projSettingsDir = path15.join(settingsDir, normalizedKey);
3438
3484
  const settingsPath = path15.join(projSettingsDir, "settings.json");
@@ -3483,7 +3529,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3483
3529
  let legacyFallbackWarned = false;
3484
3530
  if (!useExeAgent && !useBinSymlink) {
3485
3531
  const identityPath = path15.join(
3486
- os8.homedir(),
3532
+ os9.homedir(),
3487
3533
  ".exe-os",
3488
3534
  "identity",
3489
3535
  `${employeeName}.md`
@@ -3492,7 +3538,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3492
3538
  const hasAgentFlag = claudeSupportsAgentFlag();
3493
3539
  if (hasAgentFlag) {
3494
3540
  identityFlag = ` --agent ${employeeName}`;
3495
- } else if (existsSync11(identityPath)) {
3541
+ } else if (existsSync12(identityPath)) {
3496
3542
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
3497
3543
  legacyFallbackWarned = true;
3498
3544
  }
@@ -3513,7 +3559,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3513
3559
  }
3514
3560
  let sessionContextFlag = "";
3515
3561
  try {
3516
- const ctxDir = path15.join(os8.homedir(), ".exe-os", "session-cache");
3562
+ const ctxDir = path15.join(os9.homedir(), ".exe-os", "session-cache");
3517
3563
  mkdirSync6(ctxDir, { recursive: true });
3518
3564
  const ctxFile = path15.join(ctxDir, `session-context-${sessionName}.md`);
3519
3565
  const ctxContent = [
@@ -3673,14 +3719,14 @@ var init_tmux_routing = __esm({
3673
3719
  init_intercom_queue();
3674
3720
  init_plan_limits();
3675
3721
  init_employees();
3676
- SPAWN_LOCK_DIR = path15.join(os8.homedir(), ".exe-os", "spawn-locks");
3677
- SESSION_CACHE = path15.join(os8.homedir(), ".exe-os", "session-cache");
3722
+ SPAWN_LOCK_DIR = path15.join(os9.homedir(), ".exe-os", "spawn-locks");
3723
+ SESSION_CACHE = path15.join(os9.homedir(), ".exe-os", "session-cache");
3678
3724
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3679
3725
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
3680
3726
  VERIFY_PANE_LINES = 200;
3681
3727
  INTERCOM_DEBOUNCE_MS = 3e4;
3682
3728
  CODEX_DEBOUNCE_MS = 12e4;
3683
- INTERCOM_LOG2 = path15.join(os8.homedir(), ".exe-os", "intercom.log");
3729
+ INTERCOM_LOG2 = path15.join(os9.homedir(), ".exe-os", "intercom.log");
3684
3730
  DEBOUNCE_FILE = path15.join(SESSION_CACHE, "intercom-debounce.json");
3685
3731
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3686
3732
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
@@ -3700,6 +3746,7 @@ export {
3700
3746
  isEmployeeAlive,
3701
3747
  isExeSession,
3702
3748
  isSessionBusy,
3749
+ notifyCoordinatorTaskCompletion,
3703
3750
  notifyParentExe,
3704
3751
  parseParentExe,
3705
3752
  registerParentExe,