@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
@@ -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({
@@ -1061,13 +1106,117 @@ var init_state_bus = __esm({
1061
1106
  }
1062
1107
  });
1063
1108
 
1109
+ // src/lib/project-name.ts
1110
+ import { execSync as execSync4 } from "child_process";
1111
+ import path10 from "path";
1112
+ function getProjectName(cwd) {
1113
+ const dir = cwd ?? process.cwd();
1114
+ if (_cached2 && _cachedCwd === dir) return _cached2;
1115
+ try {
1116
+ let repoRoot;
1117
+ try {
1118
+ const gitCommonDir = execSync4("git rev-parse --path-format=absolute --git-common-dir", {
1119
+ cwd: dir,
1120
+ encoding: "utf8",
1121
+ timeout: 2e3,
1122
+ stdio: ["pipe", "pipe", "pipe"]
1123
+ }).trim();
1124
+ repoRoot = path10.dirname(gitCommonDir);
1125
+ } catch {
1126
+ repoRoot = execSync4("git rev-parse --show-toplevel", {
1127
+ cwd: dir,
1128
+ encoding: "utf8",
1129
+ timeout: 2e3,
1130
+ stdio: ["pipe", "pipe", "pipe"]
1131
+ }).trim();
1132
+ }
1133
+ _cached2 = path10.basename(repoRoot);
1134
+ _cachedCwd = dir;
1135
+ return _cached2;
1136
+ } catch {
1137
+ _cached2 = path10.basename(dir);
1138
+ _cachedCwd = dir;
1139
+ return _cached2;
1140
+ }
1141
+ }
1142
+ var _cached2, _cachedCwd;
1143
+ var init_project_name = __esm({
1144
+ "src/lib/project-name.ts"() {
1145
+ "use strict";
1146
+ _cached2 = null;
1147
+ _cachedCwd = null;
1148
+ }
1149
+ });
1150
+
1151
+ // src/lib/session-scope.ts
1152
+ var session_scope_exports = {};
1153
+ __export(session_scope_exports, {
1154
+ assertSessionScope: () => assertSessionScope,
1155
+ findSessionForProject: () => findSessionForProject,
1156
+ getSessionProject: () => getSessionProject
1157
+ });
1158
+ function getSessionProject(sessionName) {
1159
+ const sessions = listSessions();
1160
+ const entry = sessions.find((s) => s.windowName === sessionName);
1161
+ if (!entry) return null;
1162
+ const parts = entry.projectDir.split("/").filter(Boolean);
1163
+ return parts[parts.length - 1] ?? null;
1164
+ }
1165
+ function findSessionForProject(projectName) {
1166
+ const sessions = listSessions();
1167
+ for (const s of sessions) {
1168
+ const proj = s.projectDir.split("/").filter(Boolean).pop();
1169
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
1170
+ }
1171
+ return null;
1172
+ }
1173
+ function assertSessionScope(actionType, targetProject) {
1174
+ try {
1175
+ const currentProject = getProjectName();
1176
+ const exeSession = resolveExeSession();
1177
+ if (!exeSession) {
1178
+ return { allowed: true, reason: "no_session" };
1179
+ }
1180
+ if (currentProject === targetProject) {
1181
+ return {
1182
+ allowed: true,
1183
+ reason: "same_session",
1184
+ currentProject,
1185
+ targetProject
1186
+ };
1187
+ }
1188
+ process.stderr.write(
1189
+ `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
1190
+ `
1191
+ );
1192
+ return {
1193
+ allowed: false,
1194
+ reason: "cross_session_denied",
1195
+ currentProject,
1196
+ targetProject,
1197
+ targetSession: findSessionForProject(targetProject)?.windowName
1198
+ };
1199
+ } catch {
1200
+ return { allowed: true, reason: "no_session" };
1201
+ }
1202
+ }
1203
+ var init_session_scope = __esm({
1204
+ "src/lib/session-scope.ts"() {
1205
+ "use strict";
1206
+ init_session_registry();
1207
+ init_project_name();
1208
+ init_tmux_routing();
1209
+ init_employees();
1210
+ }
1211
+ });
1212
+
1064
1213
  // src/lib/tasks-crud.ts
1065
1214
  import crypto3 from "crypto";
1066
- import path10 from "path";
1067
- import os7 from "os";
1068
- import { execSync as execSync4 } from "child_process";
1215
+ import path11 from "path";
1216
+ import os8 from "os";
1217
+ import { execSync as execSync5 } from "child_process";
1069
1218
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1070
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1219
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
1071
1220
  async function writeCheckpoint(input) {
1072
1221
  const client = getClient();
1073
1222
  const row = await resolveTask(client, input.taskId);
@@ -1187,9 +1336,24 @@ async function createTaskCore(input) {
1187
1336
  const now = (/* @__PURE__ */ new Date()).toISOString();
1188
1337
  const slug = slugify(input.title);
1189
1338
  let earlySessionScope = null;
1339
+ let scopeMismatchWarning;
1190
1340
  try {
1191
1341
  const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1192
- earlySessionScope = resolveExeSession2();
1342
+ const resolved = resolveExeSession2();
1343
+ if (resolved && input.projectName) {
1344
+ const { getSessionProject: getSessionProject2 } = await Promise.resolve().then(() => (init_session_scope(), session_scope_exports));
1345
+ const sessionProject = getSessionProject2(resolved);
1346
+ if (sessionProject && sessionProject !== input.projectName) {
1347
+ scopeMismatchWarning = `session/project mismatch: session "${resolved}" owns "${sessionProject}" but task targets "${input.projectName}". Routed to default scope.`;
1348
+ process.stderr.write(`[create_task] ${scopeMismatchWarning}
1349
+ `);
1350
+ earlySessionScope = null;
1351
+ } else {
1352
+ earlySessionScope = resolved;
1353
+ }
1354
+ } else {
1355
+ earlySessionScope = resolved;
1356
+ }
1193
1357
  } catch {
1194
1358
  }
1195
1359
  const scope = earlySessionScope ?? "default";
@@ -1240,10 +1404,14 @@ async function createTaskCore(input) {
1240
1404
  ${laneWarning}` : laneWarning;
1241
1405
  }
1242
1406
  }
1407
+ if (scopeMismatchWarning) {
1408
+ warning = warning ? `${warning}
1409
+ ${scopeMismatchWarning}` : scopeMismatchWarning;
1410
+ }
1243
1411
  if (input.baseDir) {
1244
1412
  try {
1245
- await mkdir3(path10.join(input.baseDir, "exe", "output"), { recursive: true });
1246
- await mkdir3(path10.join(input.baseDir, "exe", "research"), { recursive: true });
1413
+ await mkdir3(path11.join(input.baseDir, "exe", "output"), { recursive: true });
1414
+ await mkdir3(path11.join(input.baseDir, "exe", "research"), { recursive: true });
1247
1415
  await ensureArchitectureDoc(input.baseDir, input.projectName);
1248
1416
  await ensureGitignoreExe(input.baseDir);
1249
1417
  } catch {
@@ -1279,13 +1447,19 @@ ${laneWarning}` : laneWarning;
1279
1447
  });
1280
1448
  if (input.baseDir) {
1281
1449
  try {
1282
- const EXE_OS_DIR = path10.join(os7.homedir(), ".exe-os");
1283
- const mdPath = path10.join(EXE_OS_DIR, taskFile);
1284
- const mdDir = path10.dirname(mdPath);
1285
- if (!existsSync9(mdDir)) await mkdir3(mdDir, { recursive: true });
1450
+ const EXE_OS_DIR = path11.join(os8.homedir(), ".exe-os");
1451
+ const mdPath = path11.join(EXE_OS_DIR, taskFile);
1452
+ const mdDir = path11.dirname(mdPath);
1453
+ if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
1286
1454
  const reviewer = input.reviewer ?? input.assignedBy;
1287
1455
  const mdContent = `# ${input.title}
1288
1456
 
1457
+ ## MANDATORY: When done
1458
+
1459
+ You MUST call update_task with status "done" and a result summary when finished.
1460
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
1461
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
1462
+
1289
1463
  **ID:** ${id}
1290
1464
  **Status:** ${initialStatus}
1291
1465
  **Priority:** ${input.priority}
@@ -1299,12 +1473,6 @@ ${laneWarning}` : laneWarning;
1299
1473
  ## Context
1300
1474
 
1301
1475
  ${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
1476
  `;
1309
1477
  await writeFile3(mdPath, mdContent, "utf-8");
1310
1478
  } catch (err) {
@@ -1386,14 +1554,14 @@ function isTmuxSessionAlive(identifier) {
1386
1554
  if (!identifier || identifier === "unknown") return true;
1387
1555
  try {
1388
1556
  if (identifier.startsWith("%")) {
1389
- const output = execSync4("tmux list-panes -a -F '#{pane_id}'", {
1557
+ const output = execSync5("tmux list-panes -a -F '#{pane_id}'", {
1390
1558
  timeout: 2e3,
1391
1559
  encoding: "utf8",
1392
1560
  stdio: ["pipe", "pipe", "pipe"]
1393
1561
  });
1394
1562
  return output.split("\n").some((l) => l.trim() === identifier);
1395
1563
  } else {
1396
- execSync4(`tmux has-session -t ${JSON.stringify(identifier)}`, {
1564
+ execSync5(`tmux has-session -t ${JSON.stringify(identifier)}`, {
1397
1565
  timeout: 2e3,
1398
1566
  stdio: ["pipe", "pipe", "pipe"]
1399
1567
  });
@@ -1402,7 +1570,7 @@ function isTmuxSessionAlive(identifier) {
1402
1570
  } catch {
1403
1571
  if (identifier.startsWith("%")) return true;
1404
1572
  try {
1405
- execSync4("tmux list-sessions", {
1573
+ execSync5("tmux list-sessions", {
1406
1574
  timeout: 2e3,
1407
1575
  stdio: ["pipe", "pipe", "pipe"]
1408
1576
  });
@@ -1417,12 +1585,12 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
1417
1585
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
1418
1586
  try {
1419
1587
  const since = new Date(taskCreatedAt).toISOString();
1420
- const branch = execSync4(
1588
+ const branch = execSync5(
1421
1589
  "git rev-parse --abbrev-ref HEAD 2>/dev/null",
1422
1590
  { encoding: "utf8", timeout: 3e3 }
1423
1591
  ).trim();
1424
1592
  const branchArg = branch && branch !== "HEAD" ? branch : "";
1425
- const commitCount = execSync4(
1593
+ const commitCount = execSync5(
1426
1594
  `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
1427
1595
  { encoding: "utf8", timeout: 5e3 }
1428
1596
  ).trim();
@@ -1553,7 +1721,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1553
1721
  await client.execute("PRAGMA wal_checkpoint(PASSIVE)");
1554
1722
  } catch {
1555
1723
  }
1556
- if (input.status === "done" || input.status === "cancelled") {
1724
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
1557
1725
  try {
1558
1726
  const { clearQueueForAgent: clearQueueForAgent2 } = await Promise.resolve().then(() => (init_intercom_queue(), intercom_queue_exports));
1559
1727
  clearQueueForAgent2(String(row.assigned_to));
@@ -1582,9 +1750,9 @@ async function deleteTaskCore(taskId, _baseDir) {
1582
1750
  return { taskFile, assignedTo, assignedBy, taskSlug };
1583
1751
  }
1584
1752
  async function ensureArchitectureDoc(baseDir, projectName) {
1585
- const archPath = path10.join(baseDir, "exe", "ARCHITECTURE.md");
1753
+ const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
1586
1754
  try {
1587
- if (existsSync9(archPath)) return;
1755
+ if (existsSync10(archPath)) return;
1588
1756
  const template = [
1589
1757
  `# ${projectName} \u2014 System Architecture`,
1590
1758
  "",
@@ -1617,9 +1785,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
1617
1785
  }
1618
1786
  }
1619
1787
  async function ensureGitignoreExe(baseDir) {
1620
- const gitignorePath = path10.join(baseDir, ".gitignore");
1788
+ const gitignorePath = path11.join(baseDir, ".gitignore");
1621
1789
  try {
1622
- if (existsSync9(gitignorePath)) {
1790
+ if (existsSync10(gitignorePath)) {
1623
1791
  const content = readFileSync9(gitignorePath, "utf-8");
1624
1792
  if (/^\/?exe\/?$/m.test(content)) return;
1625
1793
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -1651,58 +1819,42 @@ var init_tasks_crud = __esm({
1651
1819
  });
1652
1820
 
1653
1821
  // src/lib/tasks-review.ts
1654
- import path11 from "path";
1655
- import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
1822
+ import path12 from "path";
1823
+ import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
1656
1824
  async function countPendingReviews(sessionScope) {
1657
1825
  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
- }
1826
+ const scope = strictSessionScopeFilter(
1827
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1828
+ );
1665
1829
  const result = await client.execute({
1666
- sql: "SELECT COUNT(*) as cnt FROM tasks WHERE status = 'needs_review'",
1667
- args: []
1830
+ sql: `SELECT COUNT(*) as cnt FROM tasks
1831
+ WHERE status = 'needs_review'${scope.sql}`,
1832
+ args: [...scope.args]
1668
1833
  });
1669
1834
  return Number(result.rows[0]?.cnt) || 0;
1670
1835
  }
1671
1836
  async function countNewPendingReviewsSince(sinceIso, sessionScope) {
1672
1837
  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
- }
1838
+ const scope = strictSessionScopeFilter(
1839
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1840
+ );
1682
1841
  const result = await client.execute({
1683
1842
  sql: `SELECT COUNT(*) as cnt FROM tasks
1684
- WHERE status = 'needs_review' AND updated_at > ?`,
1685
- args: [sinceIso]
1843
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
1844
+ args: [sinceIso, ...scope.args]
1686
1845
  });
1687
1846
  return Number(result.rows[0]?.cnt) || 0;
1688
1847
  }
1689
1848
  async function listPendingReviews(limit, sessionScope) {
1690
1849
  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
- }
1850
+ const scope = strictSessionScopeFilter(
1851
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
1852
+ );
1701
1853
  const result = await client.execute({
1702
1854
  sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
1703
- WHERE status = 'needs_review'
1855
+ WHERE status = 'needs_review'${scope.sql}
1704
1856
  ORDER BY updated_at ASC LIMIT ?`,
1705
- args: [limit]
1857
+ args: [...scope.args, limit]
1706
1858
  });
1707
1859
  return result.rows;
1708
1860
  }
@@ -1714,7 +1866,7 @@ async function cleanupOrphanedReviews() {
1714
1866
  WHERE status IN ('open', 'needs_review', 'in_progress')
1715
1867
  AND assigned_by = 'system'
1716
1868
  AND title LIKE 'Review:%'
1717
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
1869
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
1718
1870
  args: [now]
1719
1871
  });
1720
1872
  const r1b = await client.execute({
@@ -1833,11 +1985,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
1833
1985
  );
1834
1986
  }
1835
1987
  try {
1836
- const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
1837
- if (existsSync10(cacheDir)) {
1988
+ const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
1989
+ if (existsSync11(cacheDir)) {
1838
1990
  for (const f of readdirSync2(cacheDir)) {
1839
1991
  if (f.startsWith("review-notified-")) {
1840
- unlinkSync3(path11.join(cacheDir, f));
1992
+ unlinkSync3(path12.join(cacheDir, f));
1841
1993
  }
1842
1994
  }
1843
1995
  }
@@ -1854,11 +2006,12 @@ var init_tasks_review = __esm({
1854
2006
  init_tmux_routing();
1855
2007
  init_session_key();
1856
2008
  init_state_bus();
2009
+ init_task_scope();
1857
2010
  }
1858
2011
  });
1859
2012
 
1860
2013
  // src/lib/tasks-chain.ts
1861
- import path12 from "path";
2014
+ import path13 from "path";
1862
2015
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
1863
2016
  async function cascadeUnblock(taskId, baseDir, now) {
1864
2017
  const client = getClient();
@@ -1875,7 +2028,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
1875
2028
  });
1876
2029
  for (const ur of unblockedRows.rows) {
1877
2030
  try {
1878
- const ubFile = path12.join(baseDir, String(ur.task_file));
2031
+ const ubFile = path13.join(baseDir, String(ur.task_file));
1879
2032
  let ubContent = await readFile3(ubFile, "utf-8");
1880
2033
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
1881
2034
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -1910,7 +2063,7 @@ async function checkSubtaskCompletion(parentTaskId, projectName) {
1910
2063
  const scScope = sessionScopeFilter();
1911
2064
  const remaining = await client.execute({
1912
2065
  sql: `SELECT COUNT(*) as cnt FROM tasks
1913
- WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled')${scScope.sql}`,
2066
+ WHERE parent_task_id = ? AND status NOT IN ('done', 'cancelled', 'closed')${scScope.sql}`,
1914
2067
  args: [parentTaskId, ...scScope.args]
1915
2068
  });
1916
2069
  const cnt = Number(remaining.rows[0]?.cnt ?? 1);
@@ -1942,110 +2095,6 @@ var init_tasks_chain = __esm({
1942
2095
  }
1943
2096
  });
1944
2097
 
1945
- // src/lib/project-name.ts
1946
- import { execSync as execSync5 } from "child_process";
1947
- import path13 from "path";
1948
- function getProjectName(cwd) {
1949
- const dir = cwd ?? process.cwd();
1950
- if (_cached2 && _cachedCwd === dir) return _cached2;
1951
- try {
1952
- let repoRoot;
1953
- try {
1954
- const gitCommonDir = execSync5("git rev-parse --path-format=absolute --git-common-dir", {
1955
- cwd: dir,
1956
- encoding: "utf8",
1957
- timeout: 2e3,
1958
- stdio: ["pipe", "pipe", "pipe"]
1959
- }).trim();
1960
- repoRoot = path13.dirname(gitCommonDir);
1961
- } catch {
1962
- repoRoot = execSync5("git rev-parse --show-toplevel", {
1963
- cwd: dir,
1964
- encoding: "utf8",
1965
- timeout: 2e3,
1966
- stdio: ["pipe", "pipe", "pipe"]
1967
- }).trim();
1968
- }
1969
- _cached2 = path13.basename(repoRoot);
1970
- _cachedCwd = dir;
1971
- return _cached2;
1972
- } catch {
1973
- _cached2 = path13.basename(dir);
1974
- _cachedCwd = dir;
1975
- return _cached2;
1976
- }
1977
- }
1978
- var _cached2, _cachedCwd;
1979
- var init_project_name = __esm({
1980
- "src/lib/project-name.ts"() {
1981
- "use strict";
1982
- _cached2 = null;
1983
- _cachedCwd = null;
1984
- }
1985
- });
1986
-
1987
- // src/lib/session-scope.ts
1988
- var session_scope_exports = {};
1989
- __export(session_scope_exports, {
1990
- assertSessionScope: () => assertSessionScope,
1991
- findSessionForProject: () => findSessionForProject,
1992
- getSessionProject: () => getSessionProject
1993
- });
1994
- function getSessionProject(sessionName) {
1995
- const sessions = listSessions();
1996
- const entry = sessions.find((s) => s.windowName === sessionName);
1997
- if (!entry) return null;
1998
- const parts = entry.projectDir.split("/").filter(Boolean);
1999
- return parts[parts.length - 1] ?? null;
2000
- }
2001
- function findSessionForProject(projectName) {
2002
- const sessions = listSessions();
2003
- for (const s of sessions) {
2004
- const proj = s.projectDir.split("/").filter(Boolean).pop();
2005
- if (proj === projectName && isCoordinatorName(s.agentId)) return s;
2006
- }
2007
- return null;
2008
- }
2009
- function assertSessionScope(actionType, targetProject) {
2010
- try {
2011
- const currentProject = getProjectName();
2012
- const exeSession = resolveExeSession();
2013
- if (!exeSession) {
2014
- return { allowed: true, reason: "no_session" };
2015
- }
2016
- if (currentProject === targetProject) {
2017
- return {
2018
- allowed: true,
2019
- reason: "same_session",
2020
- currentProject,
2021
- targetProject
2022
- };
2023
- }
2024
- process.stderr.write(
2025
- `[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
2026
- `
2027
- );
2028
- return {
2029
- allowed: false,
2030
- reason: "cross_session_denied",
2031
- currentProject,
2032
- targetProject,
2033
- targetSession: findSessionForProject(targetProject)?.windowName
2034
- };
2035
- } catch {
2036
- return { allowed: true, reason: "no_session" };
2037
- }
2038
- }
2039
- var init_session_scope = __esm({
2040
- "src/lib/session-scope.ts"() {
2041
- "use strict";
2042
- init_session_registry();
2043
- init_project_name();
2044
- init_tmux_routing();
2045
- init_employees();
2046
- }
2047
- });
2048
-
2049
2098
  // src/lib/tasks-notify.ts
2050
2099
  async function dispatchTaskToEmployee(input) {
2051
2100
  if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
@@ -2468,7 +2517,7 @@ async function updateTask(input) {
2468
2517
  if (input.status === "in_progress") {
2469
2518
  mkdirSync5(cacheDir, { recursive: true });
2470
2519
  writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
2471
- } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
2520
+ } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled" || input.status === "closed") {
2472
2521
  try {
2473
2522
  unlinkSync4(cachePath);
2474
2523
  } catch {
@@ -2476,10 +2525,10 @@ async function updateTask(input) {
2476
2525
  }
2477
2526
  } catch {
2478
2527
  }
2479
- if (input.status === "done") {
2528
+ if (input.status === "done" || input.status === "closed") {
2480
2529
  await cleanupReviewFile(row, taskFile, input.baseDir);
2481
2530
  }
2482
- if (input.status === "done" || input.status === "cancelled") {
2531
+ if (input.status === "done" || input.status === "cancelled" || input.status === "closed") {
2483
2532
  try {
2484
2533
  const client = getClient();
2485
2534
  const taskTitle = String(row.title);
@@ -2495,7 +2544,7 @@ async function updateTask(input) {
2495
2544
  if (!isCoordinatorName(assignedAgent)) {
2496
2545
  try {
2497
2546
  const draftClient = getClient();
2498
- if (input.status === "done") {
2547
+ if (input.status === "done" || input.status === "closed") {
2499
2548
  await draftClient.execute({
2500
2549
  sql: `UPDATE memories SET draft = 0 WHERE agent_id = ? AND draft = 1`,
2501
2550
  args: [assignedAgent]
@@ -2512,7 +2561,7 @@ async function updateTask(input) {
2512
2561
  try {
2513
2562
  const client = getClient();
2514
2563
  const cascaded = await client.execute({
2515
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
2564
+ sql: `UPDATE tasks SET status = 'closed', updated_at = ?
2516
2565
  WHERE parent_task_id = ? AND status = 'needs_review'`,
2517
2566
  args: [now, taskId]
2518
2567
  });
@@ -2525,14 +2574,14 @@ async function updateTask(input) {
2525
2574
  } catch {
2526
2575
  }
2527
2576
  }
2528
- const isTerminal = input.status === "done" || input.status === "needs_review";
2577
+ const isTerminal = input.status === "done" || input.status === "needs_review" || input.status === "closed";
2529
2578
  if (isTerminal) {
2530
2579
  const isCoordinator = isCoordinatorName(String(row.assigned_to));
2531
2580
  if (!isCoordinator) {
2532
2581
  notifyTaskDone();
2533
2582
  }
2534
2583
  await markTaskNotificationsRead(taskFile);
2535
- if (input.status === "done") {
2584
+ if (input.status === "done" || input.status === "closed") {
2536
2585
  try {
2537
2586
  await cascadeUnblock(taskId, input.baseDir, now);
2538
2587
  } catch {
@@ -2552,7 +2601,7 @@ async function updateTask(input) {
2552
2601
  }
2553
2602
  }
2554
2603
  }
2555
- if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
2604
+ if ((input.status === "done" || input.status === "closed") && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
2556
2605
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
2557
2606
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
2558
2607
  taskId,
@@ -2924,6 +2973,7 @@ __export(tmux_routing_exports, {
2924
2973
  isEmployeeAlive: () => isEmployeeAlive,
2925
2974
  isExeSession: () => isExeSession,
2926
2975
  isSessionBusy: () => isSessionBusy,
2976
+ notifyCoordinatorTaskCompletion: () => notifyCoordinatorTaskCompletion,
2927
2977
  notifyParentExe: () => notifyParentExe,
2928
2978
  parseParentExe: () => parseParentExe,
2929
2979
  registerParentExe: () => registerParentExe,
@@ -2934,9 +2984,9 @@ __export(tmux_routing_exports, {
2934
2984
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
2935
2985
  });
2936
2986
  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";
2987
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync, readdirSync as readdirSync3 } from "fs";
2938
2988
  import path15 from "path";
2939
- import os8 from "os";
2989
+ import os9 from "os";
2940
2990
  import { fileURLToPath } from "url";
2941
2991
  import { unlinkSync as unlinkSync5 } from "fs";
2942
2992
  function spawnLockPath(sessionName) {
@@ -2951,11 +3001,11 @@ function isProcessAlive(pid) {
2951
3001
  }
2952
3002
  }
2953
3003
  function acquireSpawnLock(sessionName) {
2954
- if (!existsSync11(SPAWN_LOCK_DIR)) {
3004
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
2955
3005
  mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
2956
3006
  }
2957
3007
  const lockFile = spawnLockPath(sessionName);
2958
- if (existsSync11(lockFile)) {
3008
+ if (existsSync12(lockFile)) {
2959
3009
  try {
2960
3010
  const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
2961
3011
  const age = Date.now() - lock.timestamp;
@@ -2983,7 +3033,7 @@ function resolveBehaviorsExporterScript() {
2983
3033
  "bin",
2984
3034
  "exe-export-behaviors.js"
2985
3035
  );
2986
- return existsSync11(scriptPath) ? scriptPath : null;
3036
+ return existsSync12(scriptPath) ? scriptPath : null;
2987
3037
  } catch {
2988
3038
  return null;
2989
3039
  }
@@ -3049,7 +3099,7 @@ function extractRootExe(name) {
3049
3099
  return parts.length > 0 ? parts[parts.length - 1] : null;
3050
3100
  }
3051
3101
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3052
- if (!existsSync11(SESSION_CACHE)) {
3102
+ if (!existsSync12(SESSION_CACHE)) {
3053
3103
  mkdirSync6(SESSION_CACHE, { recursive: true });
3054
3104
  }
3055
3105
  const rootExe = extractRootExe(parentExe) ?? parentExe;
@@ -3141,7 +3191,7 @@ async function verifyPaneAtCapacity(sessionName) {
3141
3191
  }
3142
3192
  function readDebounceState() {
3143
3193
  try {
3144
- if (!existsSync11(DEBOUNCE_FILE)) return {};
3194
+ if (!existsSync12(DEBOUNCE_FILE)) return {};
3145
3195
  const raw = JSON.parse(readFileSync10(DEBOUNCE_FILE, "utf8"));
3146
3196
  const state = {};
3147
3197
  for (const [key, val] of Object.entries(raw)) {
@@ -3158,7 +3208,7 @@ function readDebounceState() {
3158
3208
  }
3159
3209
  function writeDebounceState(state) {
3160
3210
  try {
3161
- if (!existsSync11(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3211
+ if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3162
3212
  writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
3163
3213
  } catch {
3164
3214
  }
@@ -3258,7 +3308,7 @@ function sendIntercom(targetSession) {
3258
3308
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
3259
3309
  const agent = baseAgentName(rawAgent);
3260
3310
  const markerPath = path15.join(SESSION_CACHE, `current-task-${agent}.json`);
3261
- if (existsSync11(markerPath)) {
3311
+ if (existsSync12(markerPath)) {
3262
3312
  logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
3263
3313
  return "debounced";
3264
3314
  }
@@ -3268,7 +3318,7 @@ function sendIntercom(targetSession) {
3268
3318
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
3269
3319
  const agent = baseAgentName(rawAgent);
3270
3320
  const taskDir = path15.join(process.cwd(), "exe", agent);
3271
- if (existsSync11(taskDir)) {
3321
+ if (existsSync12(taskDir)) {
3272
3322
  const files = readdirSync3(taskDir).filter(
3273
3323
  (f) => f.endsWith(".md") && f !== "DONE.txt"
3274
3324
  );
@@ -3328,6 +3378,21 @@ function notifyParentExe(sessionKey) {
3328
3378
  }
3329
3379
  return true;
3330
3380
  }
3381
+ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitle) {
3382
+ const transport = getTransport();
3383
+ try {
3384
+ const sessions = transport.listSessions();
3385
+ if (!sessions.includes(coordinatorSession)) return false;
3386
+ execSync6(
3387
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
3388
+ { timeout: 3e3 }
3389
+ );
3390
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}")`);
3391
+ return true;
3392
+ } catch {
3393
+ return false;
3394
+ }
3395
+ }
3331
3396
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3332
3397
  if (isCoordinatorName(employeeName)) {
3333
3398
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
@@ -3401,9 +3466,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3401
3466
  const transport = getTransport();
3402
3467
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3403
3468
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3404
- const logDir = path15.join(os8.homedir(), ".exe-os", "session-logs");
3469
+ const logDir = path15.join(os9.homedir(), ".exe-os", "session-logs");
3405
3470
  const logFile = path15.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3406
- if (!existsSync11(logDir)) {
3471
+ if (!existsSync12(logDir)) {
3407
3472
  mkdirSync6(logDir, { recursive: true });
3408
3473
  }
3409
3474
  transport.kill(sessionName);
@@ -3411,13 +3476,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3411
3476
  try {
3412
3477
  const thisFile = fileURLToPath(import.meta.url);
3413
3478
  const cleanupScript = path15.join(path15.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3414
- if (existsSync11(cleanupScript)) {
3479
+ if (existsSync12(cleanupScript)) {
3415
3480
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
3416
3481
  }
3417
3482
  } catch {
3418
3483
  }
3419
3484
  try {
3420
- const claudeJsonPath = path15.join(os8.homedir(), ".claude.json");
3485
+ const claudeJsonPath = path15.join(os9.homedir(), ".claude.json");
3421
3486
  let claudeJson = {};
3422
3487
  try {
3423
3488
  claudeJson = JSON.parse(readFileSync10(claudeJsonPath, "utf8"));
@@ -3432,7 +3497,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3432
3497
  } catch {
3433
3498
  }
3434
3499
  try {
3435
- const settingsDir = path15.join(os8.homedir(), ".claude", "projects");
3500
+ const settingsDir = path15.join(os9.homedir(), ".claude", "projects");
3436
3501
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3437
3502
  const projSettingsDir = path15.join(settingsDir, normalizedKey);
3438
3503
  const settingsPath = path15.join(projSettingsDir, "settings.json");
@@ -3483,7 +3548,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3483
3548
  let legacyFallbackWarned = false;
3484
3549
  if (!useExeAgent && !useBinSymlink) {
3485
3550
  const identityPath = path15.join(
3486
- os8.homedir(),
3551
+ os9.homedir(),
3487
3552
  ".exe-os",
3488
3553
  "identity",
3489
3554
  `${employeeName}.md`
@@ -3492,7 +3557,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3492
3557
  const hasAgentFlag = claudeSupportsAgentFlag();
3493
3558
  if (hasAgentFlag) {
3494
3559
  identityFlag = ` --agent ${employeeName}`;
3495
- } else if (existsSync11(identityPath)) {
3560
+ } else if (existsSync12(identityPath)) {
3496
3561
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
3497
3562
  legacyFallbackWarned = true;
3498
3563
  }
@@ -3513,7 +3578,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3513
3578
  }
3514
3579
  let sessionContextFlag = "";
3515
3580
  try {
3516
- const ctxDir = path15.join(os8.homedir(), ".exe-os", "session-cache");
3581
+ const ctxDir = path15.join(os9.homedir(), ".exe-os", "session-cache");
3517
3582
  mkdirSync6(ctxDir, { recursive: true });
3518
3583
  const ctxFile = path15.join(ctxDir, `session-context-${sessionName}.md`);
3519
3584
  const ctxContent = [
@@ -3673,14 +3738,14 @@ var init_tmux_routing = __esm({
3673
3738
  init_intercom_queue();
3674
3739
  init_plan_limits();
3675
3740
  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");
3741
+ SPAWN_LOCK_DIR = path15.join(os9.homedir(), ".exe-os", "spawn-locks");
3742
+ SESSION_CACHE = path15.join(os9.homedir(), ".exe-os", "session-cache");
3678
3743
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3679
3744
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
3680
3745
  VERIFY_PANE_LINES = 200;
3681
3746
  INTERCOM_DEBOUNCE_MS = 3e4;
3682
3747
  CODEX_DEBOUNCE_MS = 12e4;
3683
- INTERCOM_LOG2 = path15.join(os8.homedir(), ".exe-os", "intercom.log");
3748
+ INTERCOM_LOG2 = path15.join(os9.homedir(), ".exe-os", "intercom.log");
3684
3749
  DEBOUNCE_FILE = path15.join(SESSION_CACHE, "intercom-debounce.json");
3685
3750
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3686
3751
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
@@ -3700,6 +3765,7 @@ export {
3700
3765
  isEmployeeAlive,
3701
3766
  isExeSession,
3702
3767
  isSessionBusy,
3768
+ notifyCoordinatorTaskCompletion,
3703
3769
  notifyParentExe,
3704
3770
  parseParentExe,
3705
3771
  registerParentExe,