@askexenow/exe-os 0.8.83 → 0.8.86

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 (103) hide show
  1. package/dist/bin/backfill-conversations.js +746 -595
  2. package/dist/bin/backfill-responses.js +745 -594
  3. package/dist/bin/backfill-vectors.js +312 -226
  4. package/dist/bin/cleanup-stale-review-tasks.js +154 -21
  5. package/dist/bin/cli.js +14678 -12676
  6. package/dist/bin/exe-agent-config.js +242 -0
  7. package/dist/bin/exe-agent.js +100 -91
  8. package/dist/bin/exe-assign.js +1003 -854
  9. package/dist/bin/exe-boot.js +1420 -485
  10. package/dist/bin/exe-call.js +10 -0
  11. package/dist/bin/exe-cloud.js +29 -6
  12. package/dist/bin/exe-dispatch.js +572 -271
  13. package/dist/bin/exe-doctor.js +403 -6
  14. package/dist/bin/exe-export-behaviors.js +175 -72
  15. package/dist/bin/exe-forget.js +102 -3
  16. package/dist/bin/exe-gateway.js +796 -292
  17. package/dist/bin/exe-healthcheck.js +134 -1
  18. package/dist/bin/exe-heartbeat.js +172 -36
  19. package/dist/bin/exe-kill.js +175 -72
  20. package/dist/bin/exe-launch-agent.js +189 -76
  21. package/dist/bin/exe-link.js +927 -82
  22. package/dist/bin/exe-new-employee.js +60 -8
  23. package/dist/bin/exe-pending-messages.js +151 -19
  24. package/dist/bin/exe-pending-notifications.js +97 -2
  25. package/dist/bin/exe-pending-reviews.js +155 -22
  26. package/dist/bin/exe-rename.js +564 -23
  27. package/dist/bin/exe-review.js +231 -73
  28. package/dist/bin/exe-search.js +995 -228
  29. package/dist/bin/exe-session-cleanup.js +4930 -1664
  30. package/dist/bin/exe-settings.js +20 -5
  31. package/dist/bin/exe-start-codex.js +2598 -0
  32. package/dist/bin/exe-start.sh +15 -3
  33. package/dist/bin/exe-status.js +154 -21
  34. package/dist/bin/exe-team.js +97 -2
  35. package/dist/bin/git-sweep.js +1180 -363
  36. package/dist/bin/graph-backfill.js +175 -72
  37. package/dist/bin/graph-export.js +175 -72
  38. package/dist/bin/install.js +60 -7
  39. package/dist/bin/list-providers.js +1 -0
  40. package/dist/bin/scan-tasks.js +1185 -367
  41. package/dist/bin/setup.js +914 -270
  42. package/dist/bin/shard-migrate.js +175 -72
  43. package/dist/bin/update.js +1 -0
  44. package/dist/bin/wiki-sync.js +175 -72
  45. package/dist/gateway/index.js +792 -285
  46. package/dist/hooks/bug-report-worker.js +445 -135
  47. package/dist/hooks/commit-complete.js +1178 -361
  48. package/dist/hooks/error-recall.js +994 -228
  49. package/dist/hooks/ingest-worker.js +1799 -1234
  50. package/dist/hooks/ingest.js +3 -0
  51. package/dist/hooks/instructions-loaded.js +707 -97
  52. package/dist/hooks/notification.js +699 -89
  53. package/dist/hooks/post-compact.js +757 -109
  54. package/dist/hooks/pre-compact.js +1061 -244
  55. package/dist/hooks/pre-tool-use.js +787 -130
  56. package/dist/hooks/prompt-ingest-worker.js +242 -101
  57. package/dist/hooks/prompt-submit.js +1121 -299
  58. package/dist/hooks/response-ingest-worker.js +242 -101
  59. package/dist/hooks/session-end.js +4063 -397
  60. package/dist/hooks/session-start.js +1071 -254
  61. package/dist/hooks/stop.js +768 -120
  62. package/dist/hooks/subagent-stop.js +757 -109
  63. package/dist/hooks/summary-worker.js +1706 -1011
  64. package/dist/index.js +1821 -1098
  65. package/dist/lib/agent-config.js +167 -0
  66. package/dist/lib/cloud-sync.js +932 -88
  67. package/dist/lib/consolidation.js +2 -1
  68. package/dist/lib/database.js +642 -87
  69. package/dist/lib/db-daemon-client.js +503 -0
  70. package/dist/lib/device-registry.js +547 -7
  71. package/dist/lib/embedder.js +14 -28
  72. package/dist/lib/employee-templates.js +84 -74
  73. package/dist/lib/employees.js +9 -0
  74. package/dist/lib/exe-daemon-client.js +16 -29
  75. package/dist/lib/exe-daemon.js +2733 -1575
  76. package/dist/lib/hybrid-search.js +995 -228
  77. package/dist/lib/identity.js +87 -67
  78. package/dist/lib/keychain.js +9 -1
  79. package/dist/lib/messaging.js +103 -40
  80. package/dist/lib/reminders.js +91 -74
  81. package/dist/lib/runtime-table.js +16 -0
  82. package/dist/lib/schedules.js +96 -2
  83. package/dist/lib/session-wrappers.js +22 -0
  84. package/dist/lib/skill-learning.js +103 -85
  85. package/dist/lib/store.js +234 -73
  86. package/dist/lib/tasks.js +348 -134
  87. package/dist/lib/tmux-routing.js +422 -208
  88. package/dist/lib/token-spend.js +273 -0
  89. package/dist/lib/ws-client.js +11 -0
  90. package/dist/mcp/server.js +5742 -696
  91. package/dist/mcp/tools/complete-reminder.js +94 -77
  92. package/dist/mcp/tools/create-reminder.js +94 -77
  93. package/dist/mcp/tools/create-task.js +375 -152
  94. package/dist/mcp/tools/deactivate-behavior.js +95 -77
  95. package/dist/mcp/tools/list-reminders.js +94 -77
  96. package/dist/mcp/tools/list-tasks.js +99 -31
  97. package/dist/mcp/tools/send-message.js +108 -45
  98. package/dist/mcp/tools/update-task.js +162 -77
  99. package/dist/runtime/index.js +1075 -258
  100. package/dist/tui/App.js +1333 -506
  101. package/package.json +6 -1
  102. package/src/commands/exe/agent-config.md +27 -0
  103. package/src/commands/exe/cc-doctor.md +10 -0
@@ -273,6 +273,7 @@ __export(employees_exports, {
273
273
  DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
274
274
  EMPLOYEES_PATH: () => EMPLOYEES_PATH,
275
275
  addEmployee: () => addEmployee,
276
+ baseAgentName: () => baseAgentName,
276
277
  canCoordinate: () => canCoordinate,
277
278
  getCoordinatorEmployee: () => getCoordinatorEmployee,
278
279
  getCoordinatorName: () => getCoordinatorName,
@@ -369,6 +370,14 @@ function hasRole(agentName, role) {
369
370
  const emp = getEmployee(employees, agentName);
370
371
  return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
371
372
  }
373
+ function baseAgentName(name, employees) {
374
+ const match = name.match(/^([a-zA-Z]+)\d+$/);
375
+ if (!match) return name;
376
+ const base = match[1];
377
+ const roster = employees ?? loadEmployeesSync();
378
+ if (getEmployee(roster, base)) return base;
379
+ return name;
380
+ }
372
381
  function isMultiInstance(agentName, employees) {
373
382
  const roster = employees ?? loadEmployeesSync();
374
383
  const emp = getEmployee(roster, agentName);
@@ -470,15 +479,22 @@ function getClient() {
470
479
  if (!_resilientClient) {
471
480
  throw new Error("Database client not initialized. Call initDatabase() first.");
472
481
  }
482
+ if (process.env.EXE_IS_DAEMON === "1") {
483
+ return _resilientClient;
484
+ }
485
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
486
+ return _daemonClient;
487
+ }
473
488
  return _resilientClient;
474
489
  }
475
- var _resilientClient;
490
+ var _resilientClient, _daemonClient;
476
491
  var init_database = __esm({
477
492
  "src/lib/database.ts"() {
478
493
  "use strict";
479
494
  init_db_retry();
480
495
  init_employees();
481
496
  _resilientClient = null;
497
+ _daemonClient = null;
482
498
  }
483
499
  });
484
500
 
@@ -838,18 +854,69 @@ var init_provider_table = __esm({
838
854
  }
839
855
  });
840
856
 
841
- // src/lib/intercom-queue.ts
842
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
857
+ // src/lib/runtime-table.ts
858
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
859
+ var init_runtime_table = __esm({
860
+ "src/lib/runtime-table.ts"() {
861
+ "use strict";
862
+ RUNTIME_TABLE = {
863
+ codex: {
864
+ binary: "codex",
865
+ launchMode: "exec",
866
+ autoApproveFlag: "--full-auto",
867
+ inlineFlag: "--no-alt-screen",
868
+ apiKeyEnv: "OPENAI_API_KEY",
869
+ defaultModel: "gpt-5.4"
870
+ }
871
+ };
872
+ DEFAULT_RUNTIME = "claude";
873
+ }
874
+ });
875
+
876
+ // src/lib/agent-config.ts
877
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
843
878
  import path5 from "path";
879
+ function loadAgentConfig() {
880
+ if (!existsSync5(AGENT_CONFIG_PATH)) return {};
881
+ try {
882
+ return JSON.parse(readFileSync5(AGENT_CONFIG_PATH, "utf-8"));
883
+ } catch {
884
+ return {};
885
+ }
886
+ }
887
+ function getAgentRuntime(agentId) {
888
+ const config = loadAgentConfig();
889
+ const entry = config[agentId];
890
+ if (entry) return entry;
891
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
892
+ }
893
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
894
+ var init_agent_config = __esm({
895
+ "src/lib/agent-config.ts"() {
896
+ "use strict";
897
+ init_config();
898
+ init_runtime_table();
899
+ AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
900
+ DEFAULT_MODELS = {
901
+ claude: "claude-opus-4",
902
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
903
+ opencode: "minimax-m2.7"
904
+ };
905
+ }
906
+ });
907
+
908
+ // src/lib/intercom-queue.ts
909
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
910
+ import path6 from "path";
844
911
  import os5 from "os";
845
912
  function ensureDir() {
846
- const dir = path5.dirname(QUEUE_PATH);
847
- if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
913
+ const dir = path6.dirname(QUEUE_PATH);
914
+ if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
848
915
  }
849
916
  function readQueue() {
850
917
  try {
851
- if (!existsSync5(QUEUE_PATH)) return [];
852
- return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
918
+ if (!existsSync6(QUEUE_PATH)) return [];
919
+ return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
853
920
  } catch {
854
921
  return [];
855
922
  }
@@ -857,7 +924,7 @@ function readQueue() {
857
924
  function writeQueue(queue) {
858
925
  ensureDir();
859
926
  const tmp = `${QUEUE_PATH}.tmp`;
860
- writeFileSync3(tmp, JSON.stringify(queue, null, 2));
927
+ writeFileSync4(tmp, JSON.stringify(queue, null, 2));
861
928
  renameSync3(tmp, QUEUE_PATH);
862
929
  }
863
930
  function queueIntercom(targetSession, reason) {
@@ -881,25 +948,25 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
881
948
  var init_intercom_queue = __esm({
882
949
  "src/lib/intercom-queue.ts"() {
883
950
  "use strict";
884
- QUEUE_PATH = path5.join(os5.homedir(), ".exe-os", "intercom-queue.json");
951
+ QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
885
952
  TTL_MS = 60 * 60 * 1e3;
886
- INTERCOM_LOG = path5.join(os5.homedir(), ".exe-os", "intercom.log");
953
+ INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
887
954
  }
888
955
  });
889
956
 
890
957
  // src/lib/license.ts
891
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
958
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
892
959
  import { randomUUID } from "crypto";
893
- import path6 from "path";
960
+ import path7 from "path";
894
961
  import { jwtVerify, importSPKI } from "jose";
895
962
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
896
963
  var init_license = __esm({
897
964
  "src/lib/license.ts"() {
898
965
  "use strict";
899
966
  init_config();
900
- LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
901
- CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
902
- DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
967
+ LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
968
+ CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
969
+ DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
903
970
  PLAN_LIMITS = {
904
971
  free: { devices: 1, employees: 1, memories: 5e3 },
905
972
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -911,12 +978,12 @@ var init_license = __esm({
911
978
  });
912
979
 
913
980
  // src/lib/plan-limits.ts
914
- import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
915
- import path7 from "path";
981
+ import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
982
+ import path8 from "path";
916
983
  function getLicenseSync() {
917
984
  try {
918
- if (!existsSync7(CACHE_PATH2)) return freeLicense();
919
- const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
985
+ if (!existsSync8(CACHE_PATH2)) return freeLicense();
986
+ const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
920
987
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
921
988
  const parts = raw.token.split(".");
922
989
  if (parts.length !== 3) return freeLicense();
@@ -954,8 +1021,8 @@ function assertEmployeeLimitSync(rosterPath) {
954
1021
  const filePath = rosterPath ?? EMPLOYEES_PATH;
955
1022
  let count = 0;
956
1023
  try {
957
- if (existsSync7(filePath)) {
958
- const raw = readFileSync7(filePath, "utf8");
1024
+ if (existsSync8(filePath)) {
1025
+ const raw = readFileSync8(filePath, "utf8");
959
1026
  const employees = JSON.parse(raw);
960
1027
  count = Array.isArray(employees) ? employees.length : 0;
961
1028
  }
@@ -984,7 +1051,7 @@ var init_plan_limits = __esm({
984
1051
  this.name = "PlanLimitError";
985
1052
  }
986
1053
  };
987
- CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
1054
+ CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
988
1055
  }
989
1056
  });
990
1057
 
@@ -1332,13 +1399,13 @@ __export(tmux_routing_exports, {
1332
1399
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
1333
1400
  });
1334
1401
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
1335
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync8, appendFileSync } from "fs";
1336
- import path8 from "path";
1402
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync } from "fs";
1403
+ import path9 from "path";
1337
1404
  import os6 from "os";
1338
1405
  import { fileURLToPath } from "url";
1339
1406
  import { unlinkSync as unlinkSync3 } from "fs";
1340
1407
  function spawnLockPath(sessionName) {
1341
- return path8.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1408
+ return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1342
1409
  }
1343
1410
  function isProcessAlive(pid) {
1344
1411
  try {
@@ -1349,13 +1416,13 @@ function isProcessAlive(pid) {
1349
1416
  }
1350
1417
  }
1351
1418
  function acquireSpawnLock(sessionName) {
1352
- if (!existsSync8(SPAWN_LOCK_DIR)) {
1353
- mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
1419
+ if (!existsSync9(SPAWN_LOCK_DIR)) {
1420
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
1354
1421
  }
1355
1422
  const lockFile = spawnLockPath(sessionName);
1356
- if (existsSync8(lockFile)) {
1423
+ if (existsSync9(lockFile)) {
1357
1424
  try {
1358
- const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1425
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
1359
1426
  const age = Date.now() - lock.timestamp;
1360
1427
  if (isProcessAlive(lock.pid) && age < 6e4) {
1361
1428
  return false;
@@ -1363,7 +1430,7 @@ function acquireSpawnLock(sessionName) {
1363
1430
  } catch {
1364
1431
  }
1365
1432
  }
1366
- writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1433
+ writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1367
1434
  return true;
1368
1435
  }
1369
1436
  function releaseSpawnLock(sessionName) {
@@ -1375,13 +1442,13 @@ function releaseSpawnLock(sessionName) {
1375
1442
  function resolveBehaviorsExporterScript() {
1376
1443
  try {
1377
1444
  const thisFile = fileURLToPath(import.meta.url);
1378
- const scriptPath = path8.join(
1379
- path8.dirname(thisFile),
1445
+ const scriptPath = path9.join(
1446
+ path9.dirname(thisFile),
1380
1447
  "..",
1381
1448
  "bin",
1382
1449
  "exe-export-behaviors.js"
1383
1450
  );
1384
- return existsSync8(scriptPath) ? scriptPath : null;
1451
+ return existsSync9(scriptPath) ? scriptPath : null;
1385
1452
  } catch {
1386
1453
  return null;
1387
1454
  }
@@ -1447,12 +1514,12 @@ function extractRootExe(name) {
1447
1514
  return parts.length > 0 ? parts[parts.length - 1] : null;
1448
1515
  }
1449
1516
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1450
- if (!existsSync8(SESSION_CACHE)) {
1451
- mkdirSync4(SESSION_CACHE, { recursive: true });
1517
+ if (!existsSync9(SESSION_CACHE)) {
1518
+ mkdirSync5(SESSION_CACHE, { recursive: true });
1452
1519
  }
1453
1520
  const rootExe = extractRootExe(parentExe) ?? parentExe;
1454
- const filePath = path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1455
- writeFileSync5(filePath, JSON.stringify({
1521
+ const filePath = path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1522
+ writeFileSync6(filePath, JSON.stringify({
1456
1523
  parentExe: rootExe,
1457
1524
  dispatchedBy: dispatchedBy || rootExe,
1458
1525
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -1460,7 +1527,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1460
1527
  }
1461
1528
  function getParentExe(sessionKey) {
1462
1529
  try {
1463
- const data = JSON.parse(readFileSync8(path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1530
+ const data = JSON.parse(readFileSync9(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1464
1531
  return data.parentExe || null;
1465
1532
  } catch {
1466
1533
  return null;
@@ -1468,8 +1535,8 @@ function getParentExe(sessionKey) {
1468
1535
  }
1469
1536
  function getDispatchedBy(sessionKey) {
1470
1537
  try {
1471
- const data = JSON.parse(readFileSync8(
1472
- path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1538
+ const data = JSON.parse(readFileSync9(
1539
+ path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1473
1540
  "utf8"
1474
1541
  ));
1475
1542
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -1530,32 +1597,50 @@ async function verifyPaneAtCapacity(sessionName) {
1530
1597
  }
1531
1598
  function readDebounceState() {
1532
1599
  try {
1533
- if (!existsSync8(DEBOUNCE_FILE)) return {};
1534
- return JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
1600
+ if (!existsSync9(DEBOUNCE_FILE)) return {};
1601
+ const raw = JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
1602
+ const state = {};
1603
+ for (const [key, val] of Object.entries(raw)) {
1604
+ if (typeof val === "number") {
1605
+ state[key] = { lastSent: val, pending: 0 };
1606
+ } else if (val && typeof val === "object" && "lastSent" in val) {
1607
+ state[key] = val;
1608
+ }
1609
+ }
1610
+ return state;
1535
1611
  } catch {
1536
1612
  return {};
1537
1613
  }
1538
1614
  }
1539
1615
  function writeDebounceState(state) {
1540
1616
  try {
1541
- if (!existsSync8(SESSION_CACHE)) mkdirSync4(SESSION_CACHE, { recursive: true });
1542
- writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
1617
+ if (!existsSync9(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
1618
+ writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
1543
1619
  } catch {
1544
1620
  }
1545
1621
  }
1546
1622
  function isDebounced(targetSession) {
1547
1623
  const state = readDebounceState();
1548
- const lastSent = state[targetSession] ?? 0;
1549
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
1624
+ const entry = state[targetSession];
1625
+ const lastSent = entry?.lastSent ?? 0;
1626
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
1627
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
1628
+ state[targetSession].pending++;
1629
+ writeDebounceState(state);
1630
+ return true;
1631
+ }
1632
+ return false;
1550
1633
  }
1551
1634
  function recordDebounce(targetSession) {
1552
1635
  const state = readDebounceState();
1553
- state[targetSession] = Date.now();
1636
+ const batched = state[targetSession]?.pending ?? 0;
1637
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
1554
1638
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
1555
1639
  for (const key of Object.keys(state)) {
1556
- if ((state[key] ?? 0) < cutoff) delete state[key];
1640
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
1557
1641
  }
1558
1642
  writeDebounceState(state);
1643
+ return batched;
1559
1644
  }
1560
1645
  function logIntercom(msg) {
1561
1646
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -1600,7 +1685,7 @@ function sendIntercom(targetSession) {
1600
1685
  return "skipped_exe";
1601
1686
  }
1602
1687
  if (isDebounced(targetSession)) {
1603
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
1688
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
1604
1689
  return "debounced";
1605
1690
  }
1606
1691
  try {
@@ -1612,14 +1697,14 @@ function sendIntercom(targetSession) {
1612
1697
  const sessionState = getSessionState(targetSession);
1613
1698
  if (sessionState === "no_claude") {
1614
1699
  queueIntercom(targetSession, "claude not running in session");
1615
- recordDebounce(targetSession);
1616
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
1700
+ const batched2 = recordDebounce(targetSession);
1701
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
1617
1702
  return "queued";
1618
1703
  }
1619
1704
  if (sessionState === "thinking" || sessionState === "tool") {
1620
1705
  queueIntercom(targetSession, "session busy at send time");
1621
- recordDebounce(targetSession);
1622
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
1706
+ const batched2 = recordDebounce(targetSession);
1707
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
1623
1708
  return "queued";
1624
1709
  }
1625
1710
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -1627,8 +1712,8 @@ function sendIntercom(targetSession) {
1627
1712
  transport.sendKeys(targetSession, "q");
1628
1713
  }
1629
1714
  transport.sendKeys(targetSession, "/exe-intercom");
1630
- recordDebounce(targetSession);
1631
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
1715
+ const batched = recordDebounce(targetSession);
1716
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
1632
1717
  return "delivered";
1633
1718
  } catch {
1634
1719
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -1658,7 +1743,7 @@ function notifyParentExe(sessionKey) {
1658
1743
  return true;
1659
1744
  }
1660
1745
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1661
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
1746
+ if (isCoordinatorName(employeeName)) {
1662
1747
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
1663
1748
  }
1664
1749
  try {
@@ -1730,26 +1815,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1730
1815
  const transport = getTransport();
1731
1816
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
1732
1817
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
1733
- const logDir = path8.join(os6.homedir(), ".exe-os", "session-logs");
1734
- const logFile = path8.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1735
- if (!existsSync8(logDir)) {
1736
- mkdirSync4(logDir, { recursive: true });
1818
+ const logDir = path9.join(os6.homedir(), ".exe-os", "session-logs");
1819
+ const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1820
+ if (!existsSync9(logDir)) {
1821
+ mkdirSync5(logDir, { recursive: true });
1737
1822
  }
1738
1823
  transport.kill(sessionName);
1739
1824
  let cleanupSuffix = "";
1740
1825
  try {
1741
1826
  const thisFile = fileURLToPath(import.meta.url);
1742
- const cleanupScript = path8.join(path8.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1743
- if (existsSync8(cleanupScript)) {
1827
+ const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1828
+ if (existsSync9(cleanupScript)) {
1744
1829
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
1745
1830
  }
1746
1831
  } catch {
1747
1832
  }
1748
1833
  try {
1749
- const claudeJsonPath = path8.join(os6.homedir(), ".claude.json");
1834
+ const claudeJsonPath = path9.join(os6.homedir(), ".claude.json");
1750
1835
  let claudeJson = {};
1751
1836
  try {
1752
- claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
1837
+ claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
1753
1838
  } catch {
1754
1839
  }
1755
1840
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -1757,17 +1842,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1757
1842
  const trustDir = opts?.cwd ?? projectDir;
1758
1843
  if (!projects[trustDir]) projects[trustDir] = {};
1759
1844
  projects[trustDir].hasTrustDialogAccepted = true;
1760
- writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
1845
+ writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
1761
1846
  } catch {
1762
1847
  }
1763
1848
  try {
1764
- const settingsDir = path8.join(os6.homedir(), ".claude", "projects");
1849
+ const settingsDir = path9.join(os6.homedir(), ".claude", "projects");
1765
1850
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
1766
- const projSettingsDir = path8.join(settingsDir, normalizedKey);
1767
- const settingsPath = path8.join(projSettingsDir, "settings.json");
1851
+ const projSettingsDir = path9.join(settingsDir, normalizedKey);
1852
+ const settingsPath = path9.join(projSettingsDir, "settings.json");
1768
1853
  let settings = {};
1769
1854
  try {
1770
- settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
1855
+ settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
1771
1856
  } catch {
1772
1857
  }
1773
1858
  const perms = settings.permissions ?? {};
@@ -1795,20 +1880,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1795
1880
  if (changed) {
1796
1881
  perms.allow = allow;
1797
1882
  settings.permissions = perms;
1798
- mkdirSync4(projSettingsDir, { recursive: true });
1799
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1883
+ mkdirSync5(projSettingsDir, { recursive: true });
1884
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1800
1885
  }
1801
1886
  } catch {
1802
1887
  }
1803
1888
  const spawnCwd = opts?.cwd ?? projectDir;
1804
1889
  const useExeAgent = !!(opts?.model && opts?.provider);
1805
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
1890
+ const agentRtConfig = getAgentRuntime(employeeName);
1891
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
1892
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
1893
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
1806
1894
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
1807
1895
  let identityFlag = "";
1808
1896
  let behaviorsFlag = "";
1809
1897
  let legacyFallbackWarned = false;
1810
1898
  if (!useExeAgent && !useBinSymlink) {
1811
- const identityPath2 = path8.join(
1899
+ const identityPath2 = path9.join(
1812
1900
  os6.homedir(),
1813
1901
  ".exe-os",
1814
1902
  "identity",
@@ -1818,13 +1906,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1818
1906
  const hasAgentFlag = claudeSupportsAgentFlag();
1819
1907
  if (hasAgentFlag) {
1820
1908
  identityFlag = ` --agent ${employeeName}`;
1821
- } else if (existsSync8(identityPath2)) {
1909
+ } else if (existsSync9(identityPath2)) {
1822
1910
  identityFlag = ` --append-system-prompt-file ${identityPath2}`;
1823
1911
  legacyFallbackWarned = true;
1824
1912
  }
1825
1913
  const behaviorsFile = exportBehaviorsSync(
1826
1914
  employeeName,
1827
- path8.basename(spawnCwd),
1915
+ path9.basename(spawnCwd),
1828
1916
  sessionName
1829
1917
  );
1830
1918
  if (behaviorsFile) {
@@ -1839,16 +1927,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1839
1927
  }
1840
1928
  let sessionContextFlag = "";
1841
1929
  try {
1842
- const ctxDir = path8.join(os6.homedir(), ".exe-os", "session-cache");
1843
- mkdirSync4(ctxDir, { recursive: true });
1844
- const ctxFile = path8.join(ctxDir, `session-context-${sessionName}.md`);
1930
+ const ctxDir = path9.join(os6.homedir(), ".exe-os", "session-cache");
1931
+ mkdirSync5(ctxDir, { recursive: true });
1932
+ const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
1845
1933
  const ctxContent = [
1846
1934
  `## Session Context`,
1847
1935
  `You are running in tmux session: ${sessionName}.`,
1848
1936
  `Your parent coordinator session is ${exeSession}.`,
1849
1937
  `Your employees (if any) use the -${exeSession} suffix.`
1850
1938
  ].join("\n");
1851
- writeFileSync5(ctxFile, ctxContent);
1939
+ writeFileSync6(ctxFile, ctxContent);
1852
1940
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
1853
1941
  } catch {
1854
1942
  }
@@ -1862,9 +1950,48 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1862
1950
  }
1863
1951
  }
1864
1952
  }
1953
+ if (useCodex) {
1954
+ const codexCfg = RUNTIME_TABLE.codex;
1955
+ if (codexCfg?.apiKeyEnv) {
1956
+ const keyVal = process.env[codexCfg.apiKeyEnv];
1957
+ if (keyVal) {
1958
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
1959
+ }
1960
+ }
1961
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
1962
+ }
1963
+ if (useOpencode) {
1964
+ const ocCfg = PROVIDER_TABLE.opencode;
1965
+ if (ocCfg?.apiKeyEnv) {
1966
+ const keyVal = process.env[ocCfg.apiKeyEnv];
1967
+ if (keyVal) {
1968
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
1969
+ }
1970
+ }
1971
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
1972
+ }
1973
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
1974
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
1975
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
1976
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
1977
+ }
1978
+ }
1865
1979
  let spawnCommand;
1866
1980
  if (useExeAgent) {
1867
1981
  spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
1982
+ } else if (useCodex) {
1983
+ process.stderr.write(
1984
+ `[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
1985
+ `
1986
+ );
1987
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName}${cleanupSuffix}`;
1988
+ } else if (useOpencode) {
1989
+ const binName = `${employeeName}-opencode`;
1990
+ process.stderr.write(
1991
+ `[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
1992
+ `
1993
+ );
1994
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
1868
1995
  } else if (useBinSymlink) {
1869
1996
  const binName = `${employeeName}-${ccProvider}`;
1870
1997
  process.stderr.write(
@@ -1886,11 +2013,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1886
2013
  transport.pipeLog(sessionName, logFile);
1887
2014
  try {
1888
2015
  const mySession = getMySession();
1889
- const dispatchInfo = path8.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
1890
- writeFileSync5(dispatchInfo, JSON.stringify({
2016
+ const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
2017
+ writeFileSync6(dispatchInfo, JSON.stringify({
1891
2018
  dispatchedBy: mySession,
1892
2019
  rootExe: exeSession,
1893
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
2020
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
2021
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
2022
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
1894
2023
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1895
2024
  }));
1896
2025
  } catch {
@@ -1908,6 +2037,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1908
2037
  booted = true;
1909
2038
  break;
1910
2039
  }
2040
+ } else if (useCodex) {
2041
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
2042
+ booted = true;
2043
+ break;
2044
+ }
1911
2045
  } else {
1912
2046
  if (pane.includes("Claude Code") || pane.includes("\u276F")) {
1913
2047
  booted = true;
@@ -1919,9 +2053,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1919
2053
  }
1920
2054
  if (!booted) {
1921
2055
  releaseSpawnLock(sessionName);
1922
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
2056
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
2057
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
1923
2058
  }
1924
- if (!useExeAgent) {
2059
+ if (!useExeAgent && !useCodex) {
1925
2060
  try {
1926
2061
  transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
1927
2062
  } catch {
@@ -1948,17 +2083,19 @@ var init_tmux_routing = __esm({
1948
2083
  init_cc_agent_support();
1949
2084
  init_mcp_prefix();
1950
2085
  init_provider_table();
2086
+ init_agent_config();
2087
+ init_runtime_table();
1951
2088
  init_intercom_queue();
1952
2089
  init_plan_limits();
1953
2090
  init_employees();
1954
- SPAWN_LOCK_DIR = path8.join(os6.homedir(), ".exe-os", "spawn-locks");
1955
- SESSION_CACHE = path8.join(os6.homedir(), ".exe-os", "session-cache");
2091
+ SPAWN_LOCK_DIR = path9.join(os6.homedir(), ".exe-os", "spawn-locks");
2092
+ SESSION_CACHE = path9.join(os6.homedir(), ".exe-os", "session-cache");
1956
2093
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
1957
2094
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
1958
2095
  VERIFY_PANE_LINES = 200;
1959
2096
  INTERCOM_DEBOUNCE_MS = 3e4;
1960
- INTERCOM_LOG2 = path8.join(os6.homedir(), ".exe-os", "intercom.log");
1961
- DEBOUNCE_FILE = path8.join(SESSION_CACHE, "intercom-debounce.json");
2097
+ INTERCOM_LOG2 = path9.join(os6.homedir(), ".exe-os", "intercom.log");
2098
+ DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
1962
2099
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
1963
2100
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
1964
2101
  }
@@ -1990,10 +2127,11 @@ var init_task_scope = __esm({
1990
2127
 
1991
2128
  // src/lib/tasks-crud.ts
1992
2129
  import crypto3 from "crypto";
1993
- import path9 from "path";
2130
+ import path10 from "path";
2131
+ import os7 from "os";
1994
2132
  import { execSync as execSync5 } from "child_process";
1995
2133
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1996
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
2134
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
1997
2135
  async function writeCheckpoint(input) {
1998
2136
  const client = getClient();
1999
2137
  const row = await resolveTask(client, input.taskId);
@@ -2034,6 +2172,35 @@ function extractParentFromContext(contextBody) {
2034
2172
  function slugify(title) {
2035
2173
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2036
2174
  }
2175
+ function buildKeywordIndex() {
2176
+ const idx = /* @__PURE__ */ new Map();
2177
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
2178
+ for (const kw of keywords) {
2179
+ const existing = idx.get(kw) ?? [];
2180
+ existing.push(role);
2181
+ idx.set(kw, existing);
2182
+ }
2183
+ }
2184
+ return idx;
2185
+ }
2186
+ function checkLaneAffinity(title, context, assigneeName) {
2187
+ const employees = loadEmployeesSync();
2188
+ const employee = employees.find((e) => e.name === assigneeName);
2189
+ if (!employee) return void 0;
2190
+ const assigneeRole = employee.role;
2191
+ const text = `${title} ${context}`.toLowerCase();
2192
+ const matchedRoles = /* @__PURE__ */ new Set();
2193
+ for (const [keyword, roles] of KEYWORD_INDEX) {
2194
+ if (text.includes(keyword)) {
2195
+ for (const role of roles) matchedRoles.add(role);
2196
+ }
2197
+ }
2198
+ if (matchedRoles.size === 0) return void 0;
2199
+ if (matchedRoles.has(assigneeRole)) return void 0;
2200
+ if (assigneeRole === "COO") return void 0;
2201
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
2202
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
2203
+ }
2037
2204
  async function resolveTask(client, identifier, scopeSession) {
2038
2205
  const scope = sessionScopeFilter(scopeSession);
2039
2206
  let result = await client.execute({
@@ -2083,7 +2250,14 @@ async function createTaskCore(input) {
2083
2250
  const id = crypto3.randomUUID();
2084
2251
  const now = (/* @__PURE__ */ new Date()).toISOString();
2085
2252
  const slug = slugify(input.title);
2086
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
2253
+ let earlySessionScope = null;
2254
+ try {
2255
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2256
+ earlySessionScope = resolveExeSession2();
2257
+ } catch {
2258
+ }
2259
+ const scope = earlySessionScope ?? "default";
2260
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
2087
2261
  let blockedById = null;
2088
2262
  const initialStatus = input.blockedBy ? "blocked" : "open";
2089
2263
  if (input.blockedBy) {
@@ -2123,22 +2297,24 @@ async function createTaskCore(input) {
2123
2297
  if (dupCheck.rows.length > 0) {
2124
2298
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
2125
2299
  }
2300
+ if (!process.env.DISABLE_LANE_AFFINITY) {
2301
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
2302
+ if (laneWarning) {
2303
+ warning = warning ? `${warning}
2304
+ ${laneWarning}` : laneWarning;
2305
+ }
2306
+ }
2126
2307
  if (input.baseDir) {
2127
2308
  try {
2128
- await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
2129
- await mkdir3(path9.join(input.baseDir, "exe", "research"), { recursive: true });
2309
+ await mkdir3(path10.join(input.baseDir, "exe", "output"), { recursive: true });
2310
+ await mkdir3(path10.join(input.baseDir, "exe", "research"), { recursive: true });
2130
2311
  await ensureArchitectureDoc(input.baseDir, input.projectName);
2131
2312
  await ensureGitignoreExe(input.baseDir);
2132
2313
  } catch {
2133
2314
  }
2134
2315
  }
2135
2316
  const complexity = input.complexity ?? "standard";
2136
- let sessionScope = null;
2137
- try {
2138
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2139
- sessionScope = resolveExeSession2();
2140
- } catch {
2141
- }
2317
+ const sessionScope = earlySessionScope;
2142
2318
  await client.execute({
2143
2319
  sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, created_at, updated_at)
2144
2320
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -2165,6 +2341,43 @@ async function createTaskCore(input) {
2165
2341
  now
2166
2342
  ]
2167
2343
  });
2344
+ if (input.baseDir) {
2345
+ try {
2346
+ const EXE_OS_DIR = path10.join(os7.homedir(), ".exe-os");
2347
+ const mdPath = path10.join(EXE_OS_DIR, taskFile);
2348
+ const mdDir = path10.dirname(mdPath);
2349
+ if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2350
+ const reviewer = input.reviewer ?? input.assignedBy;
2351
+ const mdContent = `# ${input.title}
2352
+
2353
+ **ID:** ${id}
2354
+ **Status:** ${initialStatus}
2355
+ **Priority:** ${input.priority}
2356
+ **Assigned by:** ${input.assignedBy}
2357
+ **Assigned to:** ${input.assignedTo}
2358
+ **Project:** ${input.projectName}
2359
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2360
+ **Parent task:** ${parentTaskId}` : ""}
2361
+ **Reviewer:** ${reviewer}
2362
+
2363
+ ## Context
2364
+
2365
+ ${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
+ `;
2373
+ await writeFile3(mdPath, mdContent, "utf-8");
2374
+ } catch (err) {
2375
+ process.stderr.write(
2376
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
2377
+ `
2378
+ );
2379
+ }
2380
+ }
2168
2381
  return {
2169
2382
  id,
2170
2383
  title: input.title,
@@ -2357,7 +2570,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2357
2570
  return { row, taskFile, now, taskId };
2358
2571
  }
2359
2572
  }
2360
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2573
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
2361
2574
  process.stderr.write(
2362
2575
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2363
2576
  `
@@ -2422,9 +2635,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2422
2635
  return { taskFile, assignedTo, assignedBy, taskSlug };
2423
2636
  }
2424
2637
  async function ensureArchitectureDoc(baseDir, projectName) {
2425
- const archPath = path9.join(baseDir, "exe", "ARCHITECTURE.md");
2638
+ const archPath = path10.join(baseDir, "exe", "ARCHITECTURE.md");
2426
2639
  try {
2427
- if (existsSync9(archPath)) return;
2640
+ if (existsSync10(archPath)) return;
2428
2641
  const template = [
2429
2642
  `# ${projectName} \u2014 System Architecture`,
2430
2643
  "",
@@ -2457,10 +2670,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2457
2670
  }
2458
2671
  }
2459
2672
  async function ensureGitignoreExe(baseDir) {
2460
- const gitignorePath = path9.join(baseDir, ".gitignore");
2673
+ const gitignorePath = path10.join(baseDir, ".gitignore");
2461
2674
  try {
2462
- if (existsSync9(gitignorePath)) {
2463
- const content = readFileSync9(gitignorePath, "utf-8");
2675
+ if (existsSync10(gitignorePath)) {
2676
+ const content = readFileSync10(gitignorePath, "utf-8");
2464
2677
  if (/^\/?exe\/?$/m.test(content)) return;
2465
2678
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
2466
2679
  } else {
@@ -2469,20 +2682,30 @@ async function ensureGitignoreExe(baseDir) {
2469
2682
  } catch {
2470
2683
  }
2471
2684
  }
2472
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2685
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2473
2686
  var init_tasks_crud = __esm({
2474
2687
  "src/lib/tasks-crud.ts"() {
2475
2688
  "use strict";
2476
2689
  init_database();
2477
2690
  init_task_scope();
2691
+ init_employees();
2692
+ LANE_KEYWORDS = {
2693
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
2694
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
2695
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
2696
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
2697
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
2698
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
2699
+ };
2700
+ KEYWORD_INDEX = buildKeywordIndex();
2478
2701
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2479
2702
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2480
2703
  }
2481
2704
  });
2482
2705
 
2483
2706
  // src/lib/tasks-review.ts
2484
- import path10 from "path";
2485
- import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
2707
+ import path11 from "path";
2708
+ import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
2486
2709
  async function countPendingReviews(sessionScope) {
2487
2710
  const client = getClient();
2488
2711
  if (sessionScope) {
@@ -2504,7 +2727,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2504
2727
  const result2 = await client.execute({
2505
2728
  sql: `SELECT COUNT(*) as cnt FROM tasks
2506
2729
  WHERE status = 'needs_review' AND updated_at > ?
2507
- AND (session_scope = ? OR session_scope IS NULL)`,
2730
+ AND session_scope = ?`,
2508
2731
  args: [sinceIso, sessionScope]
2509
2732
  });
2510
2733
  return Number(result2.rows[0]?.cnt) || 0;
@@ -2522,7 +2745,7 @@ async function listPendingReviews(limit, sessionScope) {
2522
2745
  const result2 = await client.execute({
2523
2746
  sql: `SELECT title, assigned_to, project_name FROM tasks
2524
2747
  WHERE status = 'needs_review'
2525
- AND (session_scope = ? OR session_scope IS NULL)
2748
+ AND session_scope = ?
2526
2749
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
2527
2750
  args: [sessionScope, limit]
2528
2751
  });
@@ -2643,14 +2866,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2643
2866
  if (parts.length >= 3 && parts[0] === "review") {
2644
2867
  const agent = parts[1];
2645
2868
  const slug = parts.slice(2).join("-");
2646
- const originalTaskFile = `exe/${agent}/${slug}.md`;
2869
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
2647
2870
  const result = await client.execute({
2648
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
2649
- args: [now, originalTaskFile]
2871
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
2872
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
2650
2873
  });
2651
2874
  if (result.rowsAffected > 0) {
2652
2875
  process.stderr.write(
2653
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
2876
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
2654
2877
  `
2655
2878
  );
2656
2879
  }
@@ -2663,11 +2886,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2663
2886
  );
2664
2887
  }
2665
2888
  try {
2666
- const cacheDir = path10.join(EXE_AI_DIR, "session-cache");
2667
- if (existsSync10(cacheDir)) {
2889
+ const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
2890
+ if (existsSync11(cacheDir)) {
2668
2891
  for (const f of readdirSync2(cacheDir)) {
2669
2892
  if (f.startsWith("review-notified-")) {
2670
- unlinkSync4(path10.join(cacheDir, f));
2893
+ unlinkSync4(path11.join(cacheDir, f));
2671
2894
  }
2672
2895
  }
2673
2896
  }
@@ -2688,7 +2911,7 @@ var init_tasks_review = __esm({
2688
2911
  });
2689
2912
 
2690
2913
  // src/lib/tasks-chain.ts
2691
- import path11 from "path";
2914
+ import path12 from "path";
2692
2915
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2693
2916
  async function cascadeUnblock(taskId, baseDir, now) {
2694
2917
  const client = getClient();
@@ -2705,7 +2928,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
2705
2928
  });
2706
2929
  for (const ur of unblockedRows.rows) {
2707
2930
  try {
2708
- const ubFile = path11.join(baseDir, String(ur.task_file));
2931
+ const ubFile = path12.join(baseDir, String(ur.task_file));
2709
2932
  let ubContent = await readFile3(ubFile, "utf-8");
2710
2933
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
2711
2934
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -2774,7 +2997,7 @@ var init_tasks_chain = __esm({
2774
2997
 
2775
2998
  // src/lib/project-name.ts
2776
2999
  import { execSync as execSync6 } from "child_process";
2777
- import path12 from "path";
3000
+ import path13 from "path";
2778
3001
  function getProjectName(cwd) {
2779
3002
  const dir = cwd ?? process.cwd();
2780
3003
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2787,7 +3010,7 @@ function getProjectName(cwd) {
2787
3010
  timeout: 2e3,
2788
3011
  stdio: ["pipe", "pipe", "pipe"]
2789
3012
  }).trim();
2790
- repoRoot = path12.dirname(gitCommonDir);
3013
+ repoRoot = path13.dirname(gitCommonDir);
2791
3014
  } catch {
2792
3015
  repoRoot = execSync6("git rev-parse --show-toplevel", {
2793
3016
  cwd: dir,
@@ -2796,11 +3019,11 @@ function getProjectName(cwd) {
2796
3019
  stdio: ["pipe", "pipe", "pipe"]
2797
3020
  }).trim();
2798
3021
  }
2799
- _cached2 = path12.basename(repoRoot);
3022
+ _cached2 = path13.basename(repoRoot);
2800
3023
  _cachedCwd = dir;
2801
3024
  return _cached2;
2802
3025
  } catch {
2803
- _cached2 = path12.basename(dir);
3026
+ _cached2 = path13.basename(dir);
2804
3027
  _cachedCwd = dir;
2805
3028
  return _cached2;
2806
3029
  }
@@ -2832,7 +3055,7 @@ function findSessionForProject(projectName) {
2832
3055
  const sessions = listSessions();
2833
3056
  for (const s of sessions) {
2834
3057
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2835
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
3058
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
2836
3059
  }
2837
3060
  return null;
2838
3061
  }
@@ -2878,7 +3101,7 @@ var init_session_scope = __esm({
2878
3101
 
2879
3102
  // src/lib/tasks-notify.ts
2880
3103
  async function dispatchTaskToEmployee(input) {
2881
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
3104
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2882
3105
  let crossProject = false;
2883
3106
  if (input.projectName) {
2884
3107
  try {
@@ -3273,8 +3496,8 @@ __export(tasks_exports, {
3273
3496
  updateTaskStatus: () => updateTaskStatus,
3274
3497
  writeCheckpoint: () => writeCheckpoint
3275
3498
  });
3276
- import path13 from "path";
3277
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
3499
+ import path14 from "path";
3500
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
3278
3501
  async function createTask(input) {
3279
3502
  const result = await createTaskCore(input);
3280
3503
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -3293,11 +3516,11 @@ async function updateTask(input) {
3293
3516
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
3294
3517
  try {
3295
3518
  const agent = String(row.assigned_to);
3296
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3297
- const cachePath = path13.join(cacheDir, `current-task-${agent}.json`);
3519
+ const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
3520
+ const cachePath = path14.join(cacheDir, `current-task-${agent}.json`);
3298
3521
  if (input.status === "in_progress") {
3299
- mkdirSync5(cacheDir, { recursive: true });
3300
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3522
+ mkdirSync6(cacheDir, { recursive: true });
3523
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3301
3524
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3302
3525
  try {
3303
3526
  unlinkSync5(cachePath);
@@ -3357,7 +3580,7 @@ async function updateTask(input) {
3357
3580
  }
3358
3581
  const isTerminal = input.status === "done" || input.status === "needs_review";
3359
3582
  if (isTerminal) {
3360
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3583
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
3361
3584
  if (!isCoordinator) {
3362
3585
  notifyTaskDone();
3363
3586
  }
@@ -3382,7 +3605,7 @@ async function updateTask(input) {
3382
3605
  }
3383
3606
  }
3384
3607
  }
3385
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3608
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3386
3609
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3387
3610
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3388
3611
  taskId,
@@ -3398,7 +3621,7 @@ async function updateTask(input) {
3398
3621
  });
3399
3622
  }
3400
3623
  let nextTask;
3401
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3624
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
3402
3625
  try {
3403
3626
  nextTask = await findNextTask(String(row.assigned_to));
3404
3627
  } catch {
@@ -3463,17 +3686,17 @@ __export(identity_exports, {
3463
3686
  listIdentities: () => listIdentities,
3464
3687
  updateIdentity: () => updateIdentity
3465
3688
  });
3466
- import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
3689
+ import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
3467
3690
  import { readdirSync as readdirSync4 } from "fs";
3468
- import path15 from "path";
3691
+ import path16 from "path";
3469
3692
  import { createHash } from "crypto";
3470
3693
  function ensureDir2() {
3471
- if (!existsSync11(IDENTITY_DIR)) {
3472
- mkdirSync7(IDENTITY_DIR, { recursive: true });
3694
+ if (!existsSync12(IDENTITY_DIR)) {
3695
+ mkdirSync8(IDENTITY_DIR, { recursive: true });
3473
3696
  }
3474
3697
  }
3475
3698
  function identityPath(agentId) {
3476
- return path15.join(IDENTITY_DIR, `${agentId}.md`);
3699
+ return path16.join(IDENTITY_DIR, `${agentId}.md`);
3477
3700
  }
3478
3701
  function parseFrontmatter(raw) {
3479
3702
  const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
@@ -3514,8 +3737,8 @@ function contentHash(content) {
3514
3737
  }
3515
3738
  function getIdentity(agentId) {
3516
3739
  const filePath = identityPath(agentId);
3517
- if (!existsSync11(filePath)) return null;
3518
- const raw = readFileSync11(filePath, "utf-8");
3740
+ if (!existsSync12(filePath)) return null;
3741
+ const raw = readFileSync12(filePath, "utf-8");
3519
3742
  const { frontmatter, body } = parseFrontmatter(raw);
3520
3743
  return {
3521
3744
  agentId,
@@ -3529,7 +3752,7 @@ async function updateIdentity(agentId, content, updatedBy) {
3529
3752
  ensureDir2();
3530
3753
  const filePath = identityPath(agentId);
3531
3754
  const hash = contentHash(content);
3532
- writeFileSync8(filePath, content, "utf-8");
3755
+ writeFileSync9(filePath, content, "utf-8");
3533
3756
  try {
3534
3757
  const client = getClient();
3535
3758
  await client.execute({
@@ -3585,7 +3808,7 @@ var init_identity = __esm({
3585
3808
  "use strict";
3586
3809
  init_config();
3587
3810
  init_database();
3588
- IDENTITY_DIR = path15.join(EXE_AI_DIR, "identity");
3811
+ IDENTITY_DIR = path16.join(EXE_AI_DIR, "identity");
3589
3812
  }
3590
3813
  });
3591
3814
 
@@ -4125,16 +4348,16 @@ import { z } from "zod";
4125
4348
 
4126
4349
  // src/adapters/claude/active-agent.ts
4127
4350
  init_config();
4128
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync6, readdirSync as readdirSync3 } from "fs";
4351
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, unlinkSync as unlinkSync6, readdirSync as readdirSync3 } from "fs";
4129
4352
  import { execSync as execSync7 } from "child_process";
4130
- import path14 from "path";
4353
+ import path15 from "path";
4131
4354
 
4132
4355
  // src/adapters/claude/session-key.ts
4133
4356
  init_session_key();
4134
4357
 
4135
4358
  // src/adapters/claude/active-agent.ts
4136
4359
  init_employees();
4137
- var CACHE_DIR = path14.join(EXE_AI_DIR, "session-cache");
4360
+ var CACHE_DIR = path15.join(EXE_AI_DIR, "session-cache");
4138
4361
  var STALE_MS = 24 * 60 * 60 * 1e3;
4139
4362
  function isNameWithOptionalInstance(candidate, baseName) {
4140
4363
  if (candidate === baseName) return true;
@@ -4179,12 +4402,12 @@ function resolveActiveAgentFromTmuxSession(sessionName) {
4179
4402
  return null;
4180
4403
  }
4181
4404
  function getMarkerPath() {
4182
- return path14.join(CACHE_DIR, `active-agent-${getSessionKey()}.json`);
4405
+ return path15.join(CACHE_DIR, `active-agent-${getSessionKey()}.json`);
4183
4406
  }
4184
4407
  function getActiveAgent() {
4185
4408
  try {
4186
4409
  const markerPath = getMarkerPath();
4187
- const raw = readFileSync10(markerPath, "utf8");
4410
+ const raw = readFileSync11(markerPath, "utf8");
4188
4411
  const data = JSON.parse(raw);
4189
4412
  if (data.agentId) {
4190
4413
  if (data.startedAt) {
@@ -4269,10 +4492,10 @@ function registerCreateTask(server) {
4269
4492
  skipDispatch: true
4270
4493
  });
4271
4494
  try {
4272
- const { existsSync: existsSync12, mkdirSync: mkdirSync8, writeFileSync: writeFileSync9 } = await import("fs");
4495
+ const { existsSync: existsSync13, mkdirSync: mkdirSync9, writeFileSync: writeFileSync10 } = await import("fs");
4273
4496
  const { identityPath: identityPath2 } = await Promise.resolve().then(() => (init_identity(), identity_exports));
4274
4497
  const idPath = identityPath2(assigned_to);
4275
- if (!existsSync12(idPath)) {
4498
+ if (!existsSync13(idPath)) {
4276
4499
  const { loadEmployees: loadEmployees2 } = await Promise.resolve().then(() => (init_employees(), employees_exports));
4277
4500
  const employees = await loadEmployees2();
4278
4501
  const emp = employees.find((e) => e.name === assigned_to);
@@ -4281,8 +4504,8 @@ function registerCreateTask(server) {
4281
4504
  const template = getTemplateForTitle2(emp.role);
4282
4505
  if (template) {
4283
4506
  const dir = (await import("path")).dirname(idPath);
4284
- if (!existsSync12(dir)) mkdirSync8(dir, { recursive: true });
4285
- writeFileSync9(idPath, template.replace(/^agent_id: \w+/m, `agent_id: ${assigned_to}`), "utf-8");
4507
+ if (!existsSync13(dir)) mkdirSync9(dir, { recursive: true });
4508
+ writeFileSync10(idPath, template.replace(/^agent_id: \w+/m, `agent_id: ${assigned_to}`), "utf-8");
4286
4509
  }
4287
4510
  }
4288
4511
  }