@askexenow/exe-os 0.8.85 → 0.8.87

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 (57) hide show
  1. package/dist/bin/cleanup-stale-review-tasks.js +57 -19
  2. package/dist/bin/cli.js +510 -340
  3. package/dist/bin/exe-agent-config.js +242 -0
  4. package/dist/bin/exe-agent.js +3 -3
  5. package/dist/bin/exe-boot.js +344 -346
  6. package/dist/bin/exe-dispatch.js +375 -250
  7. package/dist/bin/exe-forget.js +5 -1
  8. package/dist/bin/exe-gateway.js +260 -135
  9. package/dist/bin/exe-healthcheck.js +133 -1
  10. package/dist/bin/exe-heartbeat.js +72 -31
  11. package/dist/bin/exe-link.js +25 -2
  12. package/dist/bin/exe-new-employee.js +22 -0
  13. package/dist/bin/exe-pending-messages.js +55 -17
  14. package/dist/bin/exe-pending-reviews.js +57 -19
  15. package/dist/bin/exe-search.js +6 -2
  16. package/dist/bin/exe-session-cleanup.js +260 -135
  17. package/dist/bin/exe-start-codex.js +2598 -0
  18. package/dist/bin/exe-start.sh +15 -3
  19. package/dist/bin/exe-status.js +57 -19
  20. package/dist/bin/git-sweep.js +391 -266
  21. package/dist/bin/install.js +22 -0
  22. package/dist/bin/scan-tasks.js +394 -269
  23. package/dist/bin/setup.js +50 -5
  24. package/dist/gateway/index.js +257 -132
  25. package/dist/hooks/bug-report-worker.js +242 -117
  26. package/dist/hooks/commit-complete.js +389 -264
  27. package/dist/hooks/error-recall.js +6 -2
  28. package/dist/hooks/ingest-worker.js +314 -193
  29. package/dist/hooks/post-compact.js +84 -46
  30. package/dist/hooks/pre-compact.js +272 -147
  31. package/dist/hooks/pre-tool-use.js +104 -66
  32. package/dist/hooks/prompt-submit.js +126 -66
  33. package/dist/hooks/session-end.js +277 -152
  34. package/dist/hooks/session-start.js +70 -28
  35. package/dist/hooks/stop.js +90 -52
  36. package/dist/hooks/subagent-stop.js +84 -46
  37. package/dist/hooks/summary-worker.js +175 -114
  38. package/dist/index.js +296 -171
  39. package/dist/lib/agent-config.js +167 -0
  40. package/dist/lib/cloud-sync.js +25 -2
  41. package/dist/lib/exe-daemon.js +338 -213
  42. package/dist/lib/hybrid-search.js +7 -2
  43. package/dist/lib/messaging.js +95 -39
  44. package/dist/lib/runtime-table.js +16 -0
  45. package/dist/lib/session-wrappers.js +22 -0
  46. package/dist/lib/tasks.js +242 -117
  47. package/dist/lib/tmux-routing.js +314 -189
  48. package/dist/mcp/server.js +573 -274
  49. package/dist/mcp/tools/create-task.js +260 -135
  50. package/dist/mcp/tools/list-tasks.js +68 -30
  51. package/dist/mcp/tools/send-message.js +100 -44
  52. package/dist/mcp/tools/update-task.js +123 -67
  53. package/dist/runtime/index.js +276 -151
  54. package/dist/tui/App.js +479 -354
  55. package/package.json +1 -1
  56. package/src/commands/exe/agent-config.md +27 -0
  57. package/src/commands/exe/cc-doctor.md +10 -0
package/dist/lib/tasks.js CHANGED
@@ -641,18 +641,69 @@ var init_provider_table = __esm({
641
641
  }
642
642
  });
643
643
 
644
- // src/lib/intercom-queue.ts
645
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
644
+ // src/lib/runtime-table.ts
645
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
646
+ var init_runtime_table = __esm({
647
+ "src/lib/runtime-table.ts"() {
648
+ "use strict";
649
+ RUNTIME_TABLE = {
650
+ codex: {
651
+ binary: "codex",
652
+ launchMode: "exec",
653
+ autoApproveFlag: "--full-auto",
654
+ inlineFlag: "--no-alt-screen",
655
+ apiKeyEnv: "OPENAI_API_KEY",
656
+ defaultModel: "gpt-5.4"
657
+ }
658
+ };
659
+ DEFAULT_RUNTIME = "claude";
660
+ }
661
+ });
662
+
663
+ // src/lib/agent-config.ts
664
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
646
665
  import path5 from "path";
666
+ function loadAgentConfig() {
667
+ if (!existsSync5(AGENT_CONFIG_PATH)) return {};
668
+ try {
669
+ return JSON.parse(readFileSync5(AGENT_CONFIG_PATH, "utf-8"));
670
+ } catch {
671
+ return {};
672
+ }
673
+ }
674
+ function getAgentRuntime(agentId) {
675
+ const config = loadAgentConfig();
676
+ const entry = config[agentId];
677
+ if (entry) return entry;
678
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
679
+ }
680
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
681
+ var init_agent_config = __esm({
682
+ "src/lib/agent-config.ts"() {
683
+ "use strict";
684
+ init_config();
685
+ init_runtime_table();
686
+ AGENT_CONFIG_PATH = path5.join(EXE_AI_DIR, "agent-config.json");
687
+ DEFAULT_MODELS = {
688
+ claude: "claude-opus-4",
689
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
690
+ opencode: "minimax-m2.7"
691
+ };
692
+ }
693
+ });
694
+
695
+ // src/lib/intercom-queue.ts
696
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
697
+ import path6 from "path";
647
698
  import os5 from "os";
648
699
  function ensureDir() {
649
- const dir = path5.dirname(QUEUE_PATH);
650
- if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
700
+ const dir = path6.dirname(QUEUE_PATH);
701
+ if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
651
702
  }
652
703
  function readQueue() {
653
704
  try {
654
- if (!existsSync5(QUEUE_PATH)) return [];
655
- return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
705
+ if (!existsSync6(QUEUE_PATH)) return [];
706
+ return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
656
707
  } catch {
657
708
  return [];
658
709
  }
@@ -660,7 +711,7 @@ function readQueue() {
660
711
  function writeQueue(queue) {
661
712
  ensureDir();
662
713
  const tmp = `${QUEUE_PATH}.tmp`;
663
- writeFileSync3(tmp, JSON.stringify(queue, null, 2));
714
+ writeFileSync4(tmp, JSON.stringify(queue, null, 2));
664
715
  renameSync3(tmp, QUEUE_PATH);
665
716
  }
666
717
  function queueIntercom(targetSession, reason) {
@@ -684,25 +735,25 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
684
735
  var init_intercom_queue = __esm({
685
736
  "src/lib/intercom-queue.ts"() {
686
737
  "use strict";
687
- QUEUE_PATH = path5.join(os5.homedir(), ".exe-os", "intercom-queue.json");
738
+ QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
688
739
  TTL_MS = 60 * 60 * 1e3;
689
- INTERCOM_LOG = path5.join(os5.homedir(), ".exe-os", "intercom.log");
740
+ INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
690
741
  }
691
742
  });
692
743
 
693
744
  // src/lib/license.ts
694
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
745
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
695
746
  import { randomUUID } from "crypto";
696
- import path6 from "path";
747
+ import path7 from "path";
697
748
  import { jwtVerify, importSPKI } from "jose";
698
749
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
699
750
  var init_license = __esm({
700
751
  "src/lib/license.ts"() {
701
752
  "use strict";
702
753
  init_config();
703
- LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
704
- CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
705
- DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
754
+ LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
755
+ CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
756
+ DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
706
757
  PLAN_LIMITS = {
707
758
  free: { devices: 1, employees: 1, memories: 5e3 },
708
759
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -714,12 +765,12 @@ var init_license = __esm({
714
765
  });
715
766
 
716
767
  // src/lib/plan-limits.ts
717
- import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
718
- import path7 from "path";
768
+ import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
769
+ import path8 from "path";
719
770
  function getLicenseSync() {
720
771
  try {
721
- if (!existsSync7(CACHE_PATH2)) return freeLicense();
722
- const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
772
+ if (!existsSync8(CACHE_PATH2)) return freeLicense();
773
+ const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
723
774
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
724
775
  const parts = raw.token.split(".");
725
776
  if (parts.length !== 3) return freeLicense();
@@ -757,8 +808,8 @@ function assertEmployeeLimitSync(rosterPath) {
757
808
  const filePath = rosterPath ?? EMPLOYEES_PATH;
758
809
  let count = 0;
759
810
  try {
760
- if (existsSync7(filePath)) {
761
- const raw = readFileSync7(filePath, "utf8");
811
+ if (existsSync8(filePath)) {
812
+ const raw = readFileSync8(filePath, "utf8");
762
813
  const employees = JSON.parse(raw);
763
814
  count = Array.isArray(employees) ? employees.length : 0;
764
815
  }
@@ -787,7 +838,7 @@ var init_plan_limits = __esm({
787
838
  this.name = "PlanLimitError";
788
839
  }
789
840
  };
790
- CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
841
+ CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
791
842
  }
792
843
  });
793
844
 
@@ -1135,13 +1186,13 @@ __export(tmux_routing_exports, {
1135
1186
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
1136
1187
  });
1137
1188
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
1138
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync8, appendFileSync } from "fs";
1139
- import path8 from "path";
1189
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync9, appendFileSync } from "fs";
1190
+ import path9 from "path";
1140
1191
  import os6 from "os";
1141
1192
  import { fileURLToPath } from "url";
1142
1193
  import { unlinkSync as unlinkSync3 } from "fs";
1143
1194
  function spawnLockPath(sessionName) {
1144
- return path8.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1195
+ return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1145
1196
  }
1146
1197
  function isProcessAlive(pid) {
1147
1198
  try {
@@ -1152,13 +1203,13 @@ function isProcessAlive(pid) {
1152
1203
  }
1153
1204
  }
1154
1205
  function acquireSpawnLock(sessionName) {
1155
- if (!existsSync8(SPAWN_LOCK_DIR)) {
1156
- mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
1206
+ if (!existsSync9(SPAWN_LOCK_DIR)) {
1207
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
1157
1208
  }
1158
1209
  const lockFile = spawnLockPath(sessionName);
1159
- if (existsSync8(lockFile)) {
1210
+ if (existsSync9(lockFile)) {
1160
1211
  try {
1161
- const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1212
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
1162
1213
  const age = Date.now() - lock.timestamp;
1163
1214
  if (isProcessAlive(lock.pid) && age < 6e4) {
1164
1215
  return false;
@@ -1166,7 +1217,7 @@ function acquireSpawnLock(sessionName) {
1166
1217
  } catch {
1167
1218
  }
1168
1219
  }
1169
- writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1220
+ writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1170
1221
  return true;
1171
1222
  }
1172
1223
  function releaseSpawnLock(sessionName) {
@@ -1178,13 +1229,13 @@ function releaseSpawnLock(sessionName) {
1178
1229
  function resolveBehaviorsExporterScript() {
1179
1230
  try {
1180
1231
  const thisFile = fileURLToPath(import.meta.url);
1181
- const scriptPath = path8.join(
1182
- path8.dirname(thisFile),
1232
+ const scriptPath = path9.join(
1233
+ path9.dirname(thisFile),
1183
1234
  "..",
1184
1235
  "bin",
1185
1236
  "exe-export-behaviors.js"
1186
1237
  );
1187
- return existsSync8(scriptPath) ? scriptPath : null;
1238
+ return existsSync9(scriptPath) ? scriptPath : null;
1188
1239
  } catch {
1189
1240
  return null;
1190
1241
  }
@@ -1250,12 +1301,12 @@ function extractRootExe(name) {
1250
1301
  return parts.length > 0 ? parts[parts.length - 1] : null;
1251
1302
  }
1252
1303
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1253
- if (!existsSync8(SESSION_CACHE)) {
1254
- mkdirSync4(SESSION_CACHE, { recursive: true });
1304
+ if (!existsSync9(SESSION_CACHE)) {
1305
+ mkdirSync5(SESSION_CACHE, { recursive: true });
1255
1306
  }
1256
1307
  const rootExe = extractRootExe(parentExe) ?? parentExe;
1257
- const filePath = path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1258
- writeFileSync5(filePath, JSON.stringify({
1308
+ const filePath = path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1309
+ writeFileSync6(filePath, JSON.stringify({
1259
1310
  parentExe: rootExe,
1260
1311
  dispatchedBy: dispatchedBy || rootExe,
1261
1312
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -1263,7 +1314,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1263
1314
  }
1264
1315
  function getParentExe(sessionKey) {
1265
1316
  try {
1266
- const data = JSON.parse(readFileSync8(path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1317
+ const data = JSON.parse(readFileSync9(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
1267
1318
  return data.parentExe || null;
1268
1319
  } catch {
1269
1320
  return null;
@@ -1271,8 +1322,8 @@ function getParentExe(sessionKey) {
1271
1322
  }
1272
1323
  function getDispatchedBy(sessionKey) {
1273
1324
  try {
1274
- const data = JSON.parse(readFileSync8(
1275
- path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1325
+ const data = JSON.parse(readFileSync9(
1326
+ path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1276
1327
  "utf8"
1277
1328
  ));
1278
1329
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -1333,32 +1384,50 @@ async function verifyPaneAtCapacity(sessionName) {
1333
1384
  }
1334
1385
  function readDebounceState() {
1335
1386
  try {
1336
- if (!existsSync8(DEBOUNCE_FILE)) return {};
1337
- return JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
1387
+ if (!existsSync9(DEBOUNCE_FILE)) return {};
1388
+ const raw = JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
1389
+ const state = {};
1390
+ for (const [key, val] of Object.entries(raw)) {
1391
+ if (typeof val === "number") {
1392
+ state[key] = { lastSent: val, pending: 0 };
1393
+ } else if (val && typeof val === "object" && "lastSent" in val) {
1394
+ state[key] = val;
1395
+ }
1396
+ }
1397
+ return state;
1338
1398
  } catch {
1339
1399
  return {};
1340
1400
  }
1341
1401
  }
1342
1402
  function writeDebounceState(state) {
1343
1403
  try {
1344
- if (!existsSync8(SESSION_CACHE)) mkdirSync4(SESSION_CACHE, { recursive: true });
1345
- writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
1404
+ if (!existsSync9(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
1405
+ writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
1346
1406
  } catch {
1347
1407
  }
1348
1408
  }
1349
1409
  function isDebounced(targetSession) {
1350
1410
  const state = readDebounceState();
1351
- const lastSent = state[targetSession] ?? 0;
1352
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
1411
+ const entry = state[targetSession];
1412
+ const lastSent = entry?.lastSent ?? 0;
1413
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
1414
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
1415
+ state[targetSession].pending++;
1416
+ writeDebounceState(state);
1417
+ return true;
1418
+ }
1419
+ return false;
1353
1420
  }
1354
1421
  function recordDebounce(targetSession) {
1355
1422
  const state = readDebounceState();
1356
- state[targetSession] = Date.now();
1423
+ const batched = state[targetSession]?.pending ?? 0;
1424
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
1357
1425
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
1358
1426
  for (const key of Object.keys(state)) {
1359
- if ((state[key] ?? 0) < cutoff) delete state[key];
1427
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
1360
1428
  }
1361
1429
  writeDebounceState(state);
1430
+ return batched;
1362
1431
  }
1363
1432
  function logIntercom(msg) {
1364
1433
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -1403,7 +1472,7 @@ function sendIntercom(targetSession) {
1403
1472
  return "skipped_exe";
1404
1473
  }
1405
1474
  if (isDebounced(targetSession)) {
1406
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
1475
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
1407
1476
  return "debounced";
1408
1477
  }
1409
1478
  try {
@@ -1415,14 +1484,14 @@ function sendIntercom(targetSession) {
1415
1484
  const sessionState = getSessionState(targetSession);
1416
1485
  if (sessionState === "no_claude") {
1417
1486
  queueIntercom(targetSession, "claude not running in session");
1418
- recordDebounce(targetSession);
1419
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
1487
+ const batched2 = recordDebounce(targetSession);
1488
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
1420
1489
  return "queued";
1421
1490
  }
1422
1491
  if (sessionState === "thinking" || sessionState === "tool") {
1423
1492
  queueIntercom(targetSession, "session busy at send time");
1424
- recordDebounce(targetSession);
1425
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
1493
+ const batched2 = recordDebounce(targetSession);
1494
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
1426
1495
  return "queued";
1427
1496
  }
1428
1497
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -1430,8 +1499,8 @@ function sendIntercom(targetSession) {
1430
1499
  transport.sendKeys(targetSession, "q");
1431
1500
  }
1432
1501
  transport.sendKeys(targetSession, "/exe-intercom");
1433
- recordDebounce(targetSession);
1434
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
1502
+ const batched = recordDebounce(targetSession);
1503
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
1435
1504
  return "delivered";
1436
1505
  } catch {
1437
1506
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -1533,26 +1602,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1533
1602
  const transport = getTransport();
1534
1603
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
1535
1604
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
1536
- const logDir = path8.join(os6.homedir(), ".exe-os", "session-logs");
1537
- const logFile = path8.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1538
- if (!existsSync8(logDir)) {
1539
- mkdirSync4(logDir, { recursive: true });
1605
+ const logDir = path9.join(os6.homedir(), ".exe-os", "session-logs");
1606
+ const logFile = path9.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1607
+ if (!existsSync9(logDir)) {
1608
+ mkdirSync5(logDir, { recursive: true });
1540
1609
  }
1541
1610
  transport.kill(sessionName);
1542
1611
  let cleanupSuffix = "";
1543
1612
  try {
1544
1613
  const thisFile = fileURLToPath(import.meta.url);
1545
- const cleanupScript = path8.join(path8.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1546
- if (existsSync8(cleanupScript)) {
1614
+ const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1615
+ if (existsSync9(cleanupScript)) {
1547
1616
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
1548
1617
  }
1549
1618
  } catch {
1550
1619
  }
1551
1620
  try {
1552
- const claudeJsonPath = path8.join(os6.homedir(), ".claude.json");
1621
+ const claudeJsonPath = path9.join(os6.homedir(), ".claude.json");
1553
1622
  let claudeJson = {};
1554
1623
  try {
1555
- claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
1624
+ claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
1556
1625
  } catch {
1557
1626
  }
1558
1627
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -1560,17 +1629,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1560
1629
  const trustDir = opts?.cwd ?? projectDir;
1561
1630
  if (!projects[trustDir]) projects[trustDir] = {};
1562
1631
  projects[trustDir].hasTrustDialogAccepted = true;
1563
- writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
1632
+ writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
1564
1633
  } catch {
1565
1634
  }
1566
1635
  try {
1567
- const settingsDir = path8.join(os6.homedir(), ".claude", "projects");
1636
+ const settingsDir = path9.join(os6.homedir(), ".claude", "projects");
1568
1637
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
1569
- const projSettingsDir = path8.join(settingsDir, normalizedKey);
1570
- const settingsPath = path8.join(projSettingsDir, "settings.json");
1638
+ const projSettingsDir = path9.join(settingsDir, normalizedKey);
1639
+ const settingsPath = path9.join(projSettingsDir, "settings.json");
1571
1640
  let settings = {};
1572
1641
  try {
1573
- settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
1642
+ settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
1574
1643
  } catch {
1575
1644
  }
1576
1645
  const perms = settings.permissions ?? {};
@@ -1598,20 +1667,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1598
1667
  if (changed) {
1599
1668
  perms.allow = allow;
1600
1669
  settings.permissions = perms;
1601
- mkdirSync4(projSettingsDir, { recursive: true });
1602
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1670
+ mkdirSync5(projSettingsDir, { recursive: true });
1671
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1603
1672
  }
1604
1673
  } catch {
1605
1674
  }
1606
1675
  const spawnCwd = opts?.cwd ?? projectDir;
1607
1676
  const useExeAgent = !!(opts?.model && opts?.provider);
1608
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
1677
+ const agentRtConfig = getAgentRuntime(employeeName);
1678
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
1679
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
1680
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
1609
1681
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
1610
1682
  let identityFlag = "";
1611
1683
  let behaviorsFlag = "";
1612
1684
  let legacyFallbackWarned = false;
1613
1685
  if (!useExeAgent && !useBinSymlink) {
1614
- const identityPath = path8.join(
1686
+ const identityPath = path9.join(
1615
1687
  os6.homedir(),
1616
1688
  ".exe-os",
1617
1689
  "identity",
@@ -1621,13 +1693,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1621
1693
  const hasAgentFlag = claudeSupportsAgentFlag();
1622
1694
  if (hasAgentFlag) {
1623
1695
  identityFlag = ` --agent ${employeeName}`;
1624
- } else if (existsSync8(identityPath)) {
1696
+ } else if (existsSync9(identityPath)) {
1625
1697
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
1626
1698
  legacyFallbackWarned = true;
1627
1699
  }
1628
1700
  const behaviorsFile = exportBehaviorsSync(
1629
1701
  employeeName,
1630
- path8.basename(spawnCwd),
1702
+ path9.basename(spawnCwd),
1631
1703
  sessionName
1632
1704
  );
1633
1705
  if (behaviorsFile) {
@@ -1642,16 +1714,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1642
1714
  }
1643
1715
  let sessionContextFlag = "";
1644
1716
  try {
1645
- const ctxDir = path8.join(os6.homedir(), ".exe-os", "session-cache");
1646
- mkdirSync4(ctxDir, { recursive: true });
1647
- const ctxFile = path8.join(ctxDir, `session-context-${sessionName}.md`);
1717
+ const ctxDir = path9.join(os6.homedir(), ".exe-os", "session-cache");
1718
+ mkdirSync5(ctxDir, { recursive: true });
1719
+ const ctxFile = path9.join(ctxDir, `session-context-${sessionName}.md`);
1648
1720
  const ctxContent = [
1649
1721
  `## Session Context`,
1650
1722
  `You are running in tmux session: ${sessionName}.`,
1651
1723
  `Your parent coordinator session is ${exeSession}.`,
1652
1724
  `Your employees (if any) use the -${exeSession} suffix.`
1653
1725
  ].join("\n");
1654
- writeFileSync5(ctxFile, ctxContent);
1726
+ writeFileSync6(ctxFile, ctxContent);
1655
1727
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
1656
1728
  } catch {
1657
1729
  }
@@ -1665,9 +1737,48 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1665
1737
  }
1666
1738
  }
1667
1739
  }
1740
+ if (useCodex) {
1741
+ const codexCfg = RUNTIME_TABLE.codex;
1742
+ if (codexCfg?.apiKeyEnv) {
1743
+ const keyVal = process.env[codexCfg.apiKeyEnv];
1744
+ if (keyVal) {
1745
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
1746
+ }
1747
+ }
1748
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
1749
+ }
1750
+ if (useOpencode) {
1751
+ const ocCfg = PROVIDER_TABLE.opencode;
1752
+ if (ocCfg?.apiKeyEnv) {
1753
+ const keyVal = process.env[ocCfg.apiKeyEnv];
1754
+ if (keyVal) {
1755
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
1756
+ }
1757
+ }
1758
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
1759
+ }
1760
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
1761
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
1762
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
1763
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
1764
+ }
1765
+ }
1668
1766
  let spawnCommand;
1669
1767
  if (useExeAgent) {
1670
1768
  spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
1769
+ } else if (useCodex) {
1770
+ process.stderr.write(
1771
+ `[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
1772
+ `
1773
+ );
1774
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName}${cleanupSuffix}`;
1775
+ } else if (useOpencode) {
1776
+ const binName = `${employeeName}-opencode`;
1777
+ process.stderr.write(
1778
+ `[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
1779
+ `
1780
+ );
1781
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
1671
1782
  } else if (useBinSymlink) {
1672
1783
  const binName = `${employeeName}-${ccProvider}`;
1673
1784
  process.stderr.write(
@@ -1689,11 +1800,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1689
1800
  transport.pipeLog(sessionName, logFile);
1690
1801
  try {
1691
1802
  const mySession = getMySession();
1692
- const dispatchInfo = path8.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
1693
- writeFileSync5(dispatchInfo, JSON.stringify({
1803
+ const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
1804
+ writeFileSync6(dispatchInfo, JSON.stringify({
1694
1805
  dispatchedBy: mySession,
1695
1806
  rootExe: exeSession,
1696
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
1807
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
1808
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
1809
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
1697
1810
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1698
1811
  }));
1699
1812
  } catch {
@@ -1711,6 +1824,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1711
1824
  booted = true;
1712
1825
  break;
1713
1826
  }
1827
+ } else if (useCodex) {
1828
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
1829
+ booted = true;
1830
+ break;
1831
+ }
1714
1832
  } else {
1715
1833
  if (pane.includes("Claude Code") || pane.includes("\u276F")) {
1716
1834
  booted = true;
@@ -1722,9 +1840,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1722
1840
  }
1723
1841
  if (!booted) {
1724
1842
  releaseSpawnLock(sessionName);
1725
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
1843
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
1844
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
1726
1845
  }
1727
- if (!useExeAgent) {
1846
+ if (!useExeAgent && !useCodex) {
1728
1847
  try {
1729
1848
  transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
1730
1849
  } catch {
@@ -1751,17 +1870,19 @@ var init_tmux_routing = __esm({
1751
1870
  init_cc_agent_support();
1752
1871
  init_mcp_prefix();
1753
1872
  init_provider_table();
1873
+ init_agent_config();
1874
+ init_runtime_table();
1754
1875
  init_intercom_queue();
1755
1876
  init_plan_limits();
1756
1877
  init_employees();
1757
- SPAWN_LOCK_DIR = path8.join(os6.homedir(), ".exe-os", "spawn-locks");
1758
- SESSION_CACHE = path8.join(os6.homedir(), ".exe-os", "session-cache");
1878
+ SPAWN_LOCK_DIR = path9.join(os6.homedir(), ".exe-os", "spawn-locks");
1879
+ SESSION_CACHE = path9.join(os6.homedir(), ".exe-os", "session-cache");
1759
1880
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
1760
1881
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
1761
1882
  VERIFY_PANE_LINES = 200;
1762
1883
  INTERCOM_DEBOUNCE_MS = 3e4;
1763
- INTERCOM_LOG2 = path8.join(os6.homedir(), ".exe-os", "intercom.log");
1764
- DEBOUNCE_FILE = path8.join(SESSION_CACHE, "intercom-debounce.json");
1884
+ INTERCOM_LOG2 = path9.join(os6.homedir(), ".exe-os", "intercom.log");
1885
+ DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
1765
1886
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
1766
1887
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
1767
1888
  }
@@ -1793,11 +1914,11 @@ var init_task_scope = __esm({
1793
1914
 
1794
1915
  // src/lib/tasks-crud.ts
1795
1916
  import crypto3 from "crypto";
1796
- import path9 from "path";
1917
+ import path10 from "path";
1797
1918
  import os7 from "os";
1798
1919
  import { execSync as execSync5 } from "child_process";
1799
1920
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1800
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1921
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
1801
1922
  async function writeCheckpoint(input) {
1802
1923
  const client = getClient();
1803
1924
  const row = await resolveTask(client, input.taskId);
@@ -1972,8 +2093,8 @@ ${laneWarning}` : laneWarning;
1972
2093
  }
1973
2094
  if (input.baseDir) {
1974
2095
  try {
1975
- await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
1976
- await mkdir3(path9.join(input.baseDir, "exe", "research"), { recursive: true });
2096
+ await mkdir3(path10.join(input.baseDir, "exe", "output"), { recursive: true });
2097
+ await mkdir3(path10.join(input.baseDir, "exe", "research"), { recursive: true });
1977
2098
  await ensureArchitectureDoc(input.baseDir, input.projectName);
1978
2099
  await ensureGitignoreExe(input.baseDir);
1979
2100
  } catch {
@@ -2009,10 +2130,10 @@ ${laneWarning}` : laneWarning;
2009
2130
  });
2010
2131
  if (input.baseDir) {
2011
2132
  try {
2012
- const EXE_OS_DIR = path9.join(os7.homedir(), ".exe-os");
2013
- const mdPath = path9.join(EXE_OS_DIR, taskFile);
2014
- const mdDir = path9.dirname(mdPath);
2015
- if (!existsSync9(mdDir)) await mkdir3(mdDir, { recursive: true });
2133
+ const EXE_OS_DIR = path10.join(os7.homedir(), ".exe-os");
2134
+ const mdPath = path10.join(EXE_OS_DIR, taskFile);
2135
+ const mdDir = path10.dirname(mdPath);
2136
+ if (!existsSync10(mdDir)) await mkdir3(mdDir, { recursive: true });
2016
2137
  const reviewer = input.reviewer ?? input.assignedBy;
2017
2138
  const mdContent = `# ${input.title}
2018
2139
 
@@ -2037,7 +2158,11 @@ If you skip this, your reviewer will not know you're done and your work won't be
2037
2158
  Do NOT let a failed commit or any error prevent you from calling update_task(done).
2038
2159
  `;
2039
2160
  await writeFile3(mdPath, mdContent, "utf-8");
2040
- } catch {
2161
+ } catch (err) {
2162
+ process.stderr.write(
2163
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
2164
+ `
2165
+ );
2041
2166
  }
2042
2167
  }
2043
2168
  return {
@@ -2297,9 +2422,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2297
2422
  return { taskFile, assignedTo, assignedBy, taskSlug };
2298
2423
  }
2299
2424
  async function ensureArchitectureDoc(baseDir, projectName) {
2300
- const archPath = path9.join(baseDir, "exe", "ARCHITECTURE.md");
2425
+ const archPath = path10.join(baseDir, "exe", "ARCHITECTURE.md");
2301
2426
  try {
2302
- if (existsSync9(archPath)) return;
2427
+ if (existsSync10(archPath)) return;
2303
2428
  const template = [
2304
2429
  `# ${projectName} \u2014 System Architecture`,
2305
2430
  "",
@@ -2332,10 +2457,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2332
2457
  }
2333
2458
  }
2334
2459
  async function ensureGitignoreExe(baseDir) {
2335
- const gitignorePath = path9.join(baseDir, ".gitignore");
2460
+ const gitignorePath = path10.join(baseDir, ".gitignore");
2336
2461
  try {
2337
- if (existsSync9(gitignorePath)) {
2338
- const content = readFileSync9(gitignorePath, "utf-8");
2462
+ if (existsSync10(gitignorePath)) {
2463
+ const content = readFileSync10(gitignorePath, "utf-8");
2339
2464
  if (/^\/?exe\/?$/m.test(content)) return;
2340
2465
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
2341
2466
  } else {
@@ -2366,8 +2491,8 @@ var init_tasks_crud = __esm({
2366
2491
  });
2367
2492
 
2368
2493
  // src/lib/tasks-review.ts
2369
- import path10 from "path";
2370
- import { existsSync as existsSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
2494
+ import path11 from "path";
2495
+ import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
2371
2496
  async function countPendingReviews(sessionScope) {
2372
2497
  const client = getClient();
2373
2498
  if (sessionScope) {
@@ -2548,11 +2673,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2548
2673
  );
2549
2674
  }
2550
2675
  try {
2551
- const cacheDir = path10.join(EXE_AI_DIR, "session-cache");
2552
- if (existsSync10(cacheDir)) {
2676
+ const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
2677
+ if (existsSync11(cacheDir)) {
2553
2678
  for (const f of readdirSync2(cacheDir)) {
2554
2679
  if (f.startsWith("review-notified-")) {
2555
- unlinkSync4(path10.join(cacheDir, f));
2680
+ unlinkSync4(path11.join(cacheDir, f));
2556
2681
  }
2557
2682
  }
2558
2683
  }
@@ -2573,7 +2698,7 @@ var init_tasks_review = __esm({
2573
2698
  });
2574
2699
 
2575
2700
  // src/lib/tasks-chain.ts
2576
- import path11 from "path";
2701
+ import path12 from "path";
2577
2702
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2578
2703
  async function cascadeUnblock(taskId, baseDir, now) {
2579
2704
  const client = getClient();
@@ -2590,7 +2715,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
2590
2715
  });
2591
2716
  for (const ur of unblockedRows.rows) {
2592
2717
  try {
2593
- const ubFile = path11.join(baseDir, String(ur.task_file));
2718
+ const ubFile = path12.join(baseDir, String(ur.task_file));
2594
2719
  let ubContent = await readFile3(ubFile, "utf-8");
2595
2720
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
2596
2721
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -2659,7 +2784,7 @@ var init_tasks_chain = __esm({
2659
2784
 
2660
2785
  // src/lib/project-name.ts
2661
2786
  import { execSync as execSync6 } from "child_process";
2662
- import path12 from "path";
2787
+ import path13 from "path";
2663
2788
  function getProjectName(cwd) {
2664
2789
  const dir = cwd ?? process.cwd();
2665
2790
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2672,7 +2797,7 @@ function getProjectName(cwd) {
2672
2797
  timeout: 2e3,
2673
2798
  stdio: ["pipe", "pipe", "pipe"]
2674
2799
  }).trim();
2675
- repoRoot = path12.dirname(gitCommonDir);
2800
+ repoRoot = path13.dirname(gitCommonDir);
2676
2801
  } catch {
2677
2802
  repoRoot = execSync6("git rev-parse --show-toplevel", {
2678
2803
  cwd: dir,
@@ -2681,11 +2806,11 @@ function getProjectName(cwd) {
2681
2806
  stdio: ["pipe", "pipe", "pipe"]
2682
2807
  }).trim();
2683
2808
  }
2684
- _cached2 = path12.basename(repoRoot);
2809
+ _cached2 = path13.basename(repoRoot);
2685
2810
  _cachedCwd = dir;
2686
2811
  return _cached2;
2687
2812
  } catch {
2688
- _cached2 = path12.basename(dir);
2813
+ _cached2 = path13.basename(dir);
2689
2814
  _cachedCwd = dir;
2690
2815
  return _cached2;
2691
2816
  }
@@ -3158,8 +3283,8 @@ __export(tasks_exports, {
3158
3283
  updateTaskStatus: () => updateTaskStatus,
3159
3284
  writeCheckpoint: () => writeCheckpoint
3160
3285
  });
3161
- import path13 from "path";
3162
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, unlinkSync as unlinkSync5 } from "fs";
3286
+ import path14 from "path";
3287
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
3163
3288
  async function createTask(input) {
3164
3289
  const result = await createTaskCore(input);
3165
3290
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -3178,11 +3303,11 @@ async function updateTask(input) {
3178
3303
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
3179
3304
  try {
3180
3305
  const agent = String(row.assigned_to);
3181
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3182
- const cachePath = path13.join(cacheDir, `current-task-${agent}.json`);
3306
+ const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
3307
+ const cachePath = path14.join(cacheDir, `current-task-${agent}.json`);
3183
3308
  if (input.status === "in_progress") {
3184
- mkdirSync5(cacheDir, { recursive: true });
3185
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3309
+ mkdirSync6(cacheDir, { recursive: true });
3310
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3186
3311
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3187
3312
  try {
3188
3313
  unlinkSync5(cachePath);