@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
package/dist/lib/tasks.js CHANGED
@@ -266,15 +266,22 @@ function getClient() {
266
266
  if (!_resilientClient) {
267
267
  throw new Error("Database client not initialized. Call initDatabase() first.");
268
268
  }
269
+ if (process.env.EXE_IS_DAEMON === "1") {
270
+ return _resilientClient;
271
+ }
272
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
273
+ return _daemonClient;
274
+ }
269
275
  return _resilientClient;
270
276
  }
271
- var _resilientClient;
277
+ var _resilientClient, _daemonClient;
272
278
  var init_database = __esm({
273
279
  "src/lib/database.ts"() {
274
280
  "use strict";
275
281
  init_db_retry();
276
282
  init_employees();
277
283
  _resilientClient = null;
284
+ _daemonClient = null;
278
285
  }
279
286
  });
280
287
 
@@ -634,18 +641,69 @@ var init_provider_table = __esm({
634
641
  }
635
642
  });
636
643
 
637
- // src/lib/intercom-queue.ts
638
- 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";
639
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";
640
698
  import os5 from "os";
641
699
  function ensureDir() {
642
- const dir = path5.dirname(QUEUE_PATH);
643
- if (!existsSync5(dir)) mkdirSync2(dir, { recursive: true });
700
+ const dir = path6.dirname(QUEUE_PATH);
701
+ if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
644
702
  }
645
703
  function readQueue() {
646
704
  try {
647
- if (!existsSync5(QUEUE_PATH)) return [];
648
- return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
705
+ if (!existsSync6(QUEUE_PATH)) return [];
706
+ return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
649
707
  } catch {
650
708
  return [];
651
709
  }
@@ -653,7 +711,7 @@ function readQueue() {
653
711
  function writeQueue(queue) {
654
712
  ensureDir();
655
713
  const tmp = `${QUEUE_PATH}.tmp`;
656
- writeFileSync3(tmp, JSON.stringify(queue, null, 2));
714
+ writeFileSync4(tmp, JSON.stringify(queue, null, 2));
657
715
  renameSync3(tmp, QUEUE_PATH);
658
716
  }
659
717
  function queueIntercom(targetSession, reason) {
@@ -677,25 +735,25 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
677
735
  var init_intercom_queue = __esm({
678
736
  "src/lib/intercom-queue.ts"() {
679
737
  "use strict";
680
- QUEUE_PATH = path5.join(os5.homedir(), ".exe-os", "intercom-queue.json");
738
+ QUEUE_PATH = path6.join(os5.homedir(), ".exe-os", "intercom-queue.json");
681
739
  TTL_MS = 60 * 60 * 1e3;
682
- INTERCOM_LOG = path5.join(os5.homedir(), ".exe-os", "intercom.log");
740
+ INTERCOM_LOG = path6.join(os5.homedir(), ".exe-os", "intercom.log");
683
741
  }
684
742
  });
685
743
 
686
744
  // src/lib/license.ts
687
- 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";
688
746
  import { randomUUID } from "crypto";
689
- import path6 from "path";
747
+ import path7 from "path";
690
748
  import { jwtVerify, importSPKI } from "jose";
691
749
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
692
750
  var init_license = __esm({
693
751
  "src/lib/license.ts"() {
694
752
  "use strict";
695
753
  init_config();
696
- LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
697
- CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
698
- 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");
699
757
  PLAN_LIMITS = {
700
758
  free: { devices: 1, employees: 1, memories: 5e3 },
701
759
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -707,12 +765,12 @@ var init_license = __esm({
707
765
  });
708
766
 
709
767
  // src/lib/plan-limits.ts
710
- import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
711
- import path7 from "path";
768
+ import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
769
+ import path8 from "path";
712
770
  function getLicenseSync() {
713
771
  try {
714
- if (!existsSync7(CACHE_PATH2)) return freeLicense();
715
- const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
772
+ if (!existsSync8(CACHE_PATH2)) return freeLicense();
773
+ const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
716
774
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
717
775
  const parts = raw.token.split(".");
718
776
  if (parts.length !== 3) return freeLicense();
@@ -750,8 +808,8 @@ function assertEmployeeLimitSync(rosterPath) {
750
808
  const filePath = rosterPath ?? EMPLOYEES_PATH;
751
809
  let count = 0;
752
810
  try {
753
- if (existsSync7(filePath)) {
754
- const raw = readFileSync7(filePath, "utf8");
811
+ if (existsSync8(filePath)) {
812
+ const raw = readFileSync8(filePath, "utf8");
755
813
  const employees = JSON.parse(raw);
756
814
  count = Array.isArray(employees) ? employees.length : 0;
757
815
  }
@@ -780,7 +838,7 @@ var init_plan_limits = __esm({
780
838
  this.name = "PlanLimitError";
781
839
  }
782
840
  };
783
- CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
841
+ CACHE_PATH2 = path8.join(EXE_AI_DIR, "license-cache.json");
784
842
  }
785
843
  });
786
844
 
@@ -1128,13 +1186,13 @@ __export(tmux_routing_exports, {
1128
1186
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
1129
1187
  });
1130
1188
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
1131
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync8, appendFileSync } from "fs";
1132
- 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";
1133
1191
  import os6 from "os";
1134
1192
  import { fileURLToPath } from "url";
1135
1193
  import { unlinkSync as unlinkSync3 } from "fs";
1136
1194
  function spawnLockPath(sessionName) {
1137
- return path8.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1195
+ return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
1138
1196
  }
1139
1197
  function isProcessAlive(pid) {
1140
1198
  try {
@@ -1145,13 +1203,13 @@ function isProcessAlive(pid) {
1145
1203
  }
1146
1204
  }
1147
1205
  function acquireSpawnLock(sessionName) {
1148
- if (!existsSync8(SPAWN_LOCK_DIR)) {
1149
- mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
1206
+ if (!existsSync9(SPAWN_LOCK_DIR)) {
1207
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
1150
1208
  }
1151
1209
  const lockFile = spawnLockPath(sessionName);
1152
- if (existsSync8(lockFile)) {
1210
+ if (existsSync9(lockFile)) {
1153
1211
  try {
1154
- const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
1212
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
1155
1213
  const age = Date.now() - lock.timestamp;
1156
1214
  if (isProcessAlive(lock.pid) && age < 6e4) {
1157
1215
  return false;
@@ -1159,7 +1217,7 @@ function acquireSpawnLock(sessionName) {
1159
1217
  } catch {
1160
1218
  }
1161
1219
  }
1162
- writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1220
+ writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
1163
1221
  return true;
1164
1222
  }
1165
1223
  function releaseSpawnLock(sessionName) {
@@ -1171,13 +1229,13 @@ function releaseSpawnLock(sessionName) {
1171
1229
  function resolveBehaviorsExporterScript() {
1172
1230
  try {
1173
1231
  const thisFile = fileURLToPath(import.meta.url);
1174
- const scriptPath = path8.join(
1175
- path8.dirname(thisFile),
1232
+ const scriptPath = path9.join(
1233
+ path9.dirname(thisFile),
1176
1234
  "..",
1177
1235
  "bin",
1178
1236
  "exe-export-behaviors.js"
1179
1237
  );
1180
- return existsSync8(scriptPath) ? scriptPath : null;
1238
+ return existsSync9(scriptPath) ? scriptPath : null;
1181
1239
  } catch {
1182
1240
  return null;
1183
1241
  }
@@ -1243,12 +1301,12 @@ function extractRootExe(name) {
1243
1301
  return parts.length > 0 ? parts[parts.length - 1] : null;
1244
1302
  }
1245
1303
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1246
- if (!existsSync8(SESSION_CACHE)) {
1247
- mkdirSync4(SESSION_CACHE, { recursive: true });
1304
+ if (!existsSync9(SESSION_CACHE)) {
1305
+ mkdirSync5(SESSION_CACHE, { recursive: true });
1248
1306
  }
1249
1307
  const rootExe = extractRootExe(parentExe) ?? parentExe;
1250
- const filePath = path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1251
- writeFileSync5(filePath, JSON.stringify({
1308
+ const filePath = path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
1309
+ writeFileSync6(filePath, JSON.stringify({
1252
1310
  parentExe: rootExe,
1253
1311
  dispatchedBy: dispatchedBy || rootExe,
1254
1312
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -1256,7 +1314,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
1256
1314
  }
1257
1315
  function getParentExe(sessionKey) {
1258
1316
  try {
1259
- 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"));
1260
1318
  return data.parentExe || null;
1261
1319
  } catch {
1262
1320
  return null;
@@ -1264,8 +1322,8 @@ function getParentExe(sessionKey) {
1264
1322
  }
1265
1323
  function getDispatchedBy(sessionKey) {
1266
1324
  try {
1267
- const data = JSON.parse(readFileSync8(
1268
- path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1325
+ const data = JSON.parse(readFileSync9(
1326
+ path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
1269
1327
  "utf8"
1270
1328
  ));
1271
1329
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -1326,32 +1384,50 @@ async function verifyPaneAtCapacity(sessionName) {
1326
1384
  }
1327
1385
  function readDebounceState() {
1328
1386
  try {
1329
- if (!existsSync8(DEBOUNCE_FILE)) return {};
1330
- 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;
1331
1398
  } catch {
1332
1399
  return {};
1333
1400
  }
1334
1401
  }
1335
1402
  function writeDebounceState(state) {
1336
1403
  try {
1337
- if (!existsSync8(SESSION_CACHE)) mkdirSync4(SESSION_CACHE, { recursive: true });
1338
- writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
1404
+ if (!existsSync9(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
1405
+ writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
1339
1406
  } catch {
1340
1407
  }
1341
1408
  }
1342
1409
  function isDebounced(targetSession) {
1343
1410
  const state = readDebounceState();
1344
- const lastSent = state[targetSession] ?? 0;
1345
- 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;
1346
1420
  }
1347
1421
  function recordDebounce(targetSession) {
1348
1422
  const state = readDebounceState();
1349
- state[targetSession] = Date.now();
1423
+ const batched = state[targetSession]?.pending ?? 0;
1424
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
1350
1425
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
1351
1426
  for (const key of Object.keys(state)) {
1352
- if ((state[key] ?? 0) < cutoff) delete state[key];
1427
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
1353
1428
  }
1354
1429
  writeDebounceState(state);
1430
+ return batched;
1355
1431
  }
1356
1432
  function logIntercom(msg) {
1357
1433
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -1396,7 +1472,7 @@ function sendIntercom(targetSession) {
1396
1472
  return "skipped_exe";
1397
1473
  }
1398
1474
  if (isDebounced(targetSession)) {
1399
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
1475
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
1400
1476
  return "debounced";
1401
1477
  }
1402
1478
  try {
@@ -1408,14 +1484,14 @@ function sendIntercom(targetSession) {
1408
1484
  const sessionState = getSessionState(targetSession);
1409
1485
  if (sessionState === "no_claude") {
1410
1486
  queueIntercom(targetSession, "claude not running in session");
1411
- recordDebounce(targetSession);
1412
- 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]` : ""}`);
1413
1489
  return "queued";
1414
1490
  }
1415
1491
  if (sessionState === "thinking" || sessionState === "tool") {
1416
1492
  queueIntercom(targetSession, "session busy at send time");
1417
- recordDebounce(targetSession);
1418
- 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]` : ""}`);
1419
1495
  return "queued";
1420
1496
  }
1421
1497
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -1423,8 +1499,8 @@ function sendIntercom(targetSession) {
1423
1499
  transport.sendKeys(targetSession, "q");
1424
1500
  }
1425
1501
  transport.sendKeys(targetSession, "/exe-intercom");
1426
- recordDebounce(targetSession);
1427
- 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)`);
1428
1504
  return "delivered";
1429
1505
  } catch {
1430
1506
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -1454,7 +1530,7 @@ function notifyParentExe(sessionKey) {
1454
1530
  return true;
1455
1531
  }
1456
1532
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1457
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
1533
+ if (isCoordinatorName(employeeName)) {
1458
1534
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
1459
1535
  }
1460
1536
  try {
@@ -1526,26 +1602,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1526
1602
  const transport = getTransport();
1527
1603
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
1528
1604
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
1529
- const logDir = path8.join(os6.homedir(), ".exe-os", "session-logs");
1530
- const logFile = path8.join(logDir, `${instanceLabel}-${Date.now()}.log`);
1531
- if (!existsSync8(logDir)) {
1532
- 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 });
1533
1609
  }
1534
1610
  transport.kill(sessionName);
1535
1611
  let cleanupSuffix = "";
1536
1612
  try {
1537
1613
  const thisFile = fileURLToPath(import.meta.url);
1538
- const cleanupScript = path8.join(path8.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1539
- if (existsSync8(cleanupScript)) {
1614
+ const cleanupScript = path9.join(path9.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
1615
+ if (existsSync9(cleanupScript)) {
1540
1616
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
1541
1617
  }
1542
1618
  } catch {
1543
1619
  }
1544
1620
  try {
1545
- const claudeJsonPath = path8.join(os6.homedir(), ".claude.json");
1621
+ const claudeJsonPath = path9.join(os6.homedir(), ".claude.json");
1546
1622
  let claudeJson = {};
1547
1623
  try {
1548
- claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
1624
+ claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
1549
1625
  } catch {
1550
1626
  }
1551
1627
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -1553,17 +1629,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1553
1629
  const trustDir = opts?.cwd ?? projectDir;
1554
1630
  if (!projects[trustDir]) projects[trustDir] = {};
1555
1631
  projects[trustDir].hasTrustDialogAccepted = true;
1556
- writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
1632
+ writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
1557
1633
  } catch {
1558
1634
  }
1559
1635
  try {
1560
- const settingsDir = path8.join(os6.homedir(), ".claude", "projects");
1636
+ const settingsDir = path9.join(os6.homedir(), ".claude", "projects");
1561
1637
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
1562
- const projSettingsDir = path8.join(settingsDir, normalizedKey);
1563
- const settingsPath = path8.join(projSettingsDir, "settings.json");
1638
+ const projSettingsDir = path9.join(settingsDir, normalizedKey);
1639
+ const settingsPath = path9.join(projSettingsDir, "settings.json");
1564
1640
  let settings = {};
1565
1641
  try {
1566
- settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
1642
+ settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
1567
1643
  } catch {
1568
1644
  }
1569
1645
  const perms = settings.permissions ?? {};
@@ -1591,20 +1667,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1591
1667
  if (changed) {
1592
1668
  perms.allow = allow;
1593
1669
  settings.permissions = perms;
1594
- mkdirSync4(projSettingsDir, { recursive: true });
1595
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1670
+ mkdirSync5(projSettingsDir, { recursive: true });
1671
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1596
1672
  }
1597
1673
  } catch {
1598
1674
  }
1599
1675
  const spawnCwd = opts?.cwd ?? projectDir;
1600
1676
  const useExeAgent = !!(opts?.model && opts?.provider);
1601
- 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();
1602
1681
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
1603
1682
  let identityFlag = "";
1604
1683
  let behaviorsFlag = "";
1605
1684
  let legacyFallbackWarned = false;
1606
1685
  if (!useExeAgent && !useBinSymlink) {
1607
- const identityPath = path8.join(
1686
+ const identityPath = path9.join(
1608
1687
  os6.homedir(),
1609
1688
  ".exe-os",
1610
1689
  "identity",
@@ -1614,13 +1693,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1614
1693
  const hasAgentFlag = claudeSupportsAgentFlag();
1615
1694
  if (hasAgentFlag) {
1616
1695
  identityFlag = ` --agent ${employeeName}`;
1617
- } else if (existsSync8(identityPath)) {
1696
+ } else if (existsSync9(identityPath)) {
1618
1697
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
1619
1698
  legacyFallbackWarned = true;
1620
1699
  }
1621
1700
  const behaviorsFile = exportBehaviorsSync(
1622
1701
  employeeName,
1623
- path8.basename(spawnCwd),
1702
+ path9.basename(spawnCwd),
1624
1703
  sessionName
1625
1704
  );
1626
1705
  if (behaviorsFile) {
@@ -1635,16 +1714,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1635
1714
  }
1636
1715
  let sessionContextFlag = "";
1637
1716
  try {
1638
- const ctxDir = path8.join(os6.homedir(), ".exe-os", "session-cache");
1639
- mkdirSync4(ctxDir, { recursive: true });
1640
- 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`);
1641
1720
  const ctxContent = [
1642
1721
  `## Session Context`,
1643
1722
  `You are running in tmux session: ${sessionName}.`,
1644
1723
  `Your parent coordinator session is ${exeSession}.`,
1645
1724
  `Your employees (if any) use the -${exeSession} suffix.`
1646
1725
  ].join("\n");
1647
- writeFileSync5(ctxFile, ctxContent);
1726
+ writeFileSync6(ctxFile, ctxContent);
1648
1727
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
1649
1728
  } catch {
1650
1729
  }
@@ -1658,9 +1737,48 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1658
1737
  }
1659
1738
  }
1660
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
+ }
1661
1766
  let spawnCommand;
1662
1767
  if (useExeAgent) {
1663
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}`;
1664
1782
  } else if (useBinSymlink) {
1665
1783
  const binName = `${employeeName}-${ccProvider}`;
1666
1784
  process.stderr.write(
@@ -1682,11 +1800,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1682
1800
  transport.pipeLog(sessionName, logFile);
1683
1801
  try {
1684
1802
  const mySession = getMySession();
1685
- const dispatchInfo = path8.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
1686
- writeFileSync5(dispatchInfo, JSON.stringify({
1803
+ const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
1804
+ writeFileSync6(dispatchInfo, JSON.stringify({
1687
1805
  dispatchedBy: mySession,
1688
1806
  rootExe: exeSession,
1689
- 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,
1690
1810
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
1691
1811
  }));
1692
1812
  } catch {
@@ -1704,6 +1824,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1704
1824
  booted = true;
1705
1825
  break;
1706
1826
  }
1827
+ } else if (useCodex) {
1828
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
1829
+ booted = true;
1830
+ break;
1831
+ }
1707
1832
  } else {
1708
1833
  if (pane.includes("Claude Code") || pane.includes("\u276F")) {
1709
1834
  booted = true;
@@ -1715,9 +1840,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1715
1840
  }
1716
1841
  if (!booted) {
1717
1842
  releaseSpawnLock(sessionName);
1718
- 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` };
1719
1845
  }
1720
- if (!useExeAgent) {
1846
+ if (!useExeAgent && !useCodex) {
1721
1847
  try {
1722
1848
  transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
1723
1849
  } catch {
@@ -1744,17 +1870,19 @@ var init_tmux_routing = __esm({
1744
1870
  init_cc_agent_support();
1745
1871
  init_mcp_prefix();
1746
1872
  init_provider_table();
1873
+ init_agent_config();
1874
+ init_runtime_table();
1747
1875
  init_intercom_queue();
1748
1876
  init_plan_limits();
1749
1877
  init_employees();
1750
- SPAWN_LOCK_DIR = path8.join(os6.homedir(), ".exe-os", "spawn-locks");
1751
- 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");
1752
1880
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
1753
1881
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
1754
1882
  VERIFY_PANE_LINES = 200;
1755
1883
  INTERCOM_DEBOUNCE_MS = 3e4;
1756
- INTERCOM_LOG2 = path8.join(os6.homedir(), ".exe-os", "intercom.log");
1757
- 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");
1758
1886
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
1759
1887
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
1760
1888
  }
@@ -1786,10 +1914,11 @@ var init_task_scope = __esm({
1786
1914
 
1787
1915
  // src/lib/tasks-crud.ts
1788
1916
  import crypto3 from "crypto";
1789
- import path9 from "path";
1917
+ import path10 from "path";
1918
+ import os7 from "os";
1790
1919
  import { execSync as execSync5 } from "child_process";
1791
1920
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1792
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1921
+ import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
1793
1922
  async function writeCheckpoint(input) {
1794
1923
  const client = getClient();
1795
1924
  const row = await resolveTask(client, input.taskId);
@@ -1830,6 +1959,35 @@ function extractParentFromContext(contextBody) {
1830
1959
  function slugify(title) {
1831
1960
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1832
1961
  }
1962
+ function buildKeywordIndex() {
1963
+ const idx = /* @__PURE__ */ new Map();
1964
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
1965
+ for (const kw of keywords) {
1966
+ const existing = idx.get(kw) ?? [];
1967
+ existing.push(role);
1968
+ idx.set(kw, existing);
1969
+ }
1970
+ }
1971
+ return idx;
1972
+ }
1973
+ function checkLaneAffinity(title, context, assigneeName) {
1974
+ const employees = loadEmployeesSync();
1975
+ const employee = employees.find((e) => e.name === assigneeName);
1976
+ if (!employee) return void 0;
1977
+ const assigneeRole = employee.role;
1978
+ const text = `${title} ${context}`.toLowerCase();
1979
+ const matchedRoles = /* @__PURE__ */ new Set();
1980
+ for (const [keyword, roles] of KEYWORD_INDEX) {
1981
+ if (text.includes(keyword)) {
1982
+ for (const role of roles) matchedRoles.add(role);
1983
+ }
1984
+ }
1985
+ if (matchedRoles.size === 0) return void 0;
1986
+ if (matchedRoles.has(assigneeRole)) return void 0;
1987
+ if (assigneeRole === "COO") return void 0;
1988
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
1989
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
1990
+ }
1833
1991
  async function resolveTask(client, identifier, scopeSession) {
1834
1992
  const scope = sessionScopeFilter(scopeSession);
1835
1993
  let result = await client.execute({
@@ -1879,7 +2037,14 @@ async function createTaskCore(input) {
1879
2037
  const id = crypto3.randomUUID();
1880
2038
  const now = (/* @__PURE__ */ new Date()).toISOString();
1881
2039
  const slug = slugify(input.title);
1882
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
2040
+ let earlySessionScope = null;
2041
+ try {
2042
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
2043
+ earlySessionScope = resolveExeSession2();
2044
+ } catch {
2045
+ }
2046
+ const scope = earlySessionScope ?? "default";
2047
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
1883
2048
  let blockedById = null;
1884
2049
  const initialStatus = input.blockedBy ? "blocked" : "open";
1885
2050
  if (input.blockedBy) {
@@ -1919,22 +2084,24 @@ async function createTaskCore(input) {
1919
2084
  if (dupCheck.rows.length > 0) {
1920
2085
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
1921
2086
  }
2087
+ if (!process.env.DISABLE_LANE_AFFINITY) {
2088
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
2089
+ if (laneWarning) {
2090
+ warning = warning ? `${warning}
2091
+ ${laneWarning}` : laneWarning;
2092
+ }
2093
+ }
1922
2094
  if (input.baseDir) {
1923
2095
  try {
1924
- await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
1925
- 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 });
1926
2098
  await ensureArchitectureDoc(input.baseDir, input.projectName);
1927
2099
  await ensureGitignoreExe(input.baseDir);
1928
2100
  } catch {
1929
2101
  }
1930
2102
  }
1931
2103
  const complexity = input.complexity ?? "standard";
1932
- let sessionScope = null;
1933
- try {
1934
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
1935
- sessionScope = resolveExeSession2();
1936
- } catch {
1937
- }
2104
+ const sessionScope = earlySessionScope;
1938
2105
  await client.execute({
1939
2106
  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)
1940
2107
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -1961,6 +2128,43 @@ async function createTaskCore(input) {
1961
2128
  now
1962
2129
  ]
1963
2130
  });
2131
+ if (input.baseDir) {
2132
+ try {
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 });
2137
+ const reviewer = input.reviewer ?? input.assignedBy;
2138
+ const mdContent = `# ${input.title}
2139
+
2140
+ **ID:** ${id}
2141
+ **Status:** ${initialStatus}
2142
+ **Priority:** ${input.priority}
2143
+ **Assigned by:** ${input.assignedBy}
2144
+ **Assigned to:** ${input.assignedTo}
2145
+ **Project:** ${input.projectName}
2146
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
2147
+ **Parent task:** ${parentTaskId}` : ""}
2148
+ **Reviewer:** ${reviewer}
2149
+
2150
+ ## Context
2151
+
2152
+ ${input.context}
2153
+
2154
+ ## MANDATORY: When done
2155
+
2156
+ You MUST call update_task with status "done" and a result summary when finished.
2157
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
2158
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
2159
+ `;
2160
+ await writeFile3(mdPath, mdContent, "utf-8");
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
+ );
2166
+ }
2167
+ }
1964
2168
  return {
1965
2169
  id,
1966
2170
  title: input.title,
@@ -2153,7 +2357,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
2153
2357
  return { row, taskFile, now, taskId };
2154
2358
  }
2155
2359
  }
2156
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
2360
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
2157
2361
  process.stderr.write(
2158
2362
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
2159
2363
  `
@@ -2218,9 +2422,9 @@ async function deleteTaskCore(taskId, _baseDir) {
2218
2422
  return { taskFile, assignedTo, assignedBy, taskSlug };
2219
2423
  }
2220
2424
  async function ensureArchitectureDoc(baseDir, projectName) {
2221
- const archPath = path9.join(baseDir, "exe", "ARCHITECTURE.md");
2425
+ const archPath = path10.join(baseDir, "exe", "ARCHITECTURE.md");
2222
2426
  try {
2223
- if (existsSync9(archPath)) return;
2427
+ if (existsSync10(archPath)) return;
2224
2428
  const template = [
2225
2429
  `# ${projectName} \u2014 System Architecture`,
2226
2430
  "",
@@ -2253,10 +2457,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
2253
2457
  }
2254
2458
  }
2255
2459
  async function ensureGitignoreExe(baseDir) {
2256
- const gitignorePath = path9.join(baseDir, ".gitignore");
2460
+ const gitignorePath = path10.join(baseDir, ".gitignore");
2257
2461
  try {
2258
- if (existsSync9(gitignorePath)) {
2259
- const content = readFileSync9(gitignorePath, "utf-8");
2462
+ if (existsSync10(gitignorePath)) {
2463
+ const content = readFileSync10(gitignorePath, "utf-8");
2260
2464
  if (/^\/?exe\/?$/m.test(content)) return;
2261
2465
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
2262
2466
  } else {
@@ -2265,20 +2469,30 @@ async function ensureGitignoreExe(baseDir) {
2265
2469
  } catch {
2266
2470
  }
2267
2471
  }
2268
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2472
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
2269
2473
  var init_tasks_crud = __esm({
2270
2474
  "src/lib/tasks-crud.ts"() {
2271
2475
  "use strict";
2272
2476
  init_database();
2273
2477
  init_task_scope();
2478
+ init_employees();
2479
+ LANE_KEYWORDS = {
2480
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
2481
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
2482
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
2483
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
2484
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
2485
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
2486
+ };
2487
+ KEYWORD_INDEX = buildKeywordIndex();
2274
2488
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
2275
2489
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
2276
2490
  }
2277
2491
  });
2278
2492
 
2279
2493
  // src/lib/tasks-review.ts
2280
- import path10 from "path";
2281
- 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";
2282
2496
  async function countPendingReviews(sessionScope) {
2283
2497
  const client = getClient();
2284
2498
  if (sessionScope) {
@@ -2300,7 +2514,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
2300
2514
  const result2 = await client.execute({
2301
2515
  sql: `SELECT COUNT(*) as cnt FROM tasks
2302
2516
  WHERE status = 'needs_review' AND updated_at > ?
2303
- AND (session_scope = ? OR session_scope IS NULL)`,
2517
+ AND session_scope = ?`,
2304
2518
  args: [sinceIso, sessionScope]
2305
2519
  });
2306
2520
  return Number(result2.rows[0]?.cnt) || 0;
@@ -2318,7 +2532,7 @@ async function listPendingReviews(limit, sessionScope) {
2318
2532
  const result2 = await client.execute({
2319
2533
  sql: `SELECT title, assigned_to, project_name FROM tasks
2320
2534
  WHERE status = 'needs_review'
2321
- AND (session_scope = ? OR session_scope IS NULL)
2535
+ AND session_scope = ?
2322
2536
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
2323
2537
  args: [sessionScope, limit]
2324
2538
  });
@@ -2439,14 +2653,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2439
2653
  if (parts.length >= 3 && parts[0] === "review") {
2440
2654
  const agent = parts[1];
2441
2655
  const slug = parts.slice(2).join("-");
2442
- const originalTaskFile = `exe/${agent}/${slug}.md`;
2656
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
2443
2657
  const result = await client.execute({
2444
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
2445
- args: [now, originalTaskFile]
2658
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
2659
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
2446
2660
  });
2447
2661
  if (result.rowsAffected > 0) {
2448
2662
  process.stderr.write(
2449
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
2663
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
2450
2664
  `
2451
2665
  );
2452
2666
  }
@@ -2459,11 +2673,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2459
2673
  );
2460
2674
  }
2461
2675
  try {
2462
- const cacheDir = path10.join(EXE_AI_DIR, "session-cache");
2463
- if (existsSync10(cacheDir)) {
2676
+ const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
2677
+ if (existsSync11(cacheDir)) {
2464
2678
  for (const f of readdirSync2(cacheDir)) {
2465
2679
  if (f.startsWith("review-notified-")) {
2466
- unlinkSync4(path10.join(cacheDir, f));
2680
+ unlinkSync4(path11.join(cacheDir, f));
2467
2681
  }
2468
2682
  }
2469
2683
  }
@@ -2484,7 +2698,7 @@ var init_tasks_review = __esm({
2484
2698
  });
2485
2699
 
2486
2700
  // src/lib/tasks-chain.ts
2487
- import path11 from "path";
2701
+ import path12 from "path";
2488
2702
  import { readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
2489
2703
  async function cascadeUnblock(taskId, baseDir, now) {
2490
2704
  const client = getClient();
@@ -2501,7 +2715,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
2501
2715
  });
2502
2716
  for (const ur of unblockedRows.rows) {
2503
2717
  try {
2504
- const ubFile = path11.join(baseDir, String(ur.task_file));
2718
+ const ubFile = path12.join(baseDir, String(ur.task_file));
2505
2719
  let ubContent = await readFile3(ubFile, "utf-8");
2506
2720
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
2507
2721
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -2570,7 +2784,7 @@ var init_tasks_chain = __esm({
2570
2784
 
2571
2785
  // src/lib/project-name.ts
2572
2786
  import { execSync as execSync6 } from "child_process";
2573
- import path12 from "path";
2787
+ import path13 from "path";
2574
2788
  function getProjectName(cwd) {
2575
2789
  const dir = cwd ?? process.cwd();
2576
2790
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -2583,7 +2797,7 @@ function getProjectName(cwd) {
2583
2797
  timeout: 2e3,
2584
2798
  stdio: ["pipe", "pipe", "pipe"]
2585
2799
  }).trim();
2586
- repoRoot = path12.dirname(gitCommonDir);
2800
+ repoRoot = path13.dirname(gitCommonDir);
2587
2801
  } catch {
2588
2802
  repoRoot = execSync6("git rev-parse --show-toplevel", {
2589
2803
  cwd: dir,
@@ -2592,11 +2806,11 @@ function getProjectName(cwd) {
2592
2806
  stdio: ["pipe", "pipe", "pipe"]
2593
2807
  }).trim();
2594
2808
  }
2595
- _cached2 = path12.basename(repoRoot);
2809
+ _cached2 = path13.basename(repoRoot);
2596
2810
  _cachedCwd = dir;
2597
2811
  return _cached2;
2598
2812
  } catch {
2599
- _cached2 = path12.basename(dir);
2813
+ _cached2 = path13.basename(dir);
2600
2814
  _cachedCwd = dir;
2601
2815
  return _cached2;
2602
2816
  }
@@ -2628,7 +2842,7 @@ function findSessionForProject(projectName) {
2628
2842
  const sessions = listSessions();
2629
2843
  for (const s of sessions) {
2630
2844
  const proj = s.projectDir.split("/").filter(Boolean).pop();
2631
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
2845
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
2632
2846
  }
2633
2847
  return null;
2634
2848
  }
@@ -2674,7 +2888,7 @@ var init_session_scope = __esm({
2674
2888
 
2675
2889
  // src/lib/tasks-notify.ts
2676
2890
  async function dispatchTaskToEmployee(input) {
2677
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2891
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
2678
2892
  let crossProject = false;
2679
2893
  if (input.projectName) {
2680
2894
  try {
@@ -3069,8 +3283,8 @@ __export(tasks_exports, {
3069
3283
  updateTaskStatus: () => updateTaskStatus,
3070
3284
  writeCheckpoint: () => writeCheckpoint
3071
3285
  });
3072
- import path13 from "path";
3073
- 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";
3074
3288
  async function createTask(input) {
3075
3289
  const result = await createTaskCore(input);
3076
3290
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -3089,11 +3303,11 @@ async function updateTask(input) {
3089
3303
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
3090
3304
  try {
3091
3305
  const agent = String(row.assigned_to);
3092
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
3093
- 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`);
3094
3308
  if (input.status === "in_progress") {
3095
- mkdirSync5(cacheDir, { recursive: true });
3096
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3309
+ mkdirSync6(cacheDir, { recursive: true });
3310
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
3097
3311
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
3098
3312
  try {
3099
3313
  unlinkSync5(cachePath);
@@ -3153,7 +3367,7 @@ async function updateTask(input) {
3153
3367
  }
3154
3368
  const isTerminal = input.status === "done" || input.status === "needs_review";
3155
3369
  if (isTerminal) {
3156
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
3370
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
3157
3371
  if (!isCoordinator) {
3158
3372
  notifyTaskDone();
3159
3373
  }
@@ -3178,7 +3392,7 @@ async function updateTask(input) {
3178
3392
  }
3179
3393
  }
3180
3394
  }
3181
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3395
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
3182
3396
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
3183
3397
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
3184
3398
  taskId,
@@ -3194,7 +3408,7 @@ async function updateTask(input) {
3194
3408
  });
3195
3409
  }
3196
3410
  let nextTask;
3197
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
3411
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
3198
3412
  try {
3199
3413
  nextTask = await findNextTask(String(row.assigned_to));
3200
3414
  } catch {