@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
@@ -264,6 +264,7 @@ __export(employees_exports, {
264
264
  DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
265
265
  EMPLOYEES_PATH: () => EMPLOYEES_PATH,
266
266
  addEmployee: () => addEmployee,
267
+ baseAgentName: () => baseAgentName,
267
268
  canCoordinate: () => canCoordinate,
268
269
  getCoordinatorEmployee: () => getCoordinatorEmployee,
269
270
  getCoordinatorName: () => getCoordinatorName,
@@ -360,6 +361,14 @@ function hasRole(agentName, role) {
360
361
  const emp = getEmployee(employees, agentName);
361
362
  return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
362
363
  }
364
+ function baseAgentName(name, employees) {
365
+ const match = name.match(/^([a-zA-Z]+)\d+$/);
366
+ if (!match) return name;
367
+ const base = match[1];
368
+ const roster = employees ?? loadEmployeesSync();
369
+ if (getEmployee(roster, base)) return base;
370
+ return name;
371
+ }
363
372
  function isMultiInstance(agentName, employees) {
364
373
  const roster = employees ?? loadEmployeesSync();
365
374
  const emp = getEmployee(roster, agentName);
@@ -476,6 +485,12 @@ function getClient() {
476
485
  if (!_resilientClient) {
477
486
  throw new Error("Database client not initialized. Call initDatabase() first.");
478
487
  }
488
+ if (process.env.EXE_IS_DAEMON === "1") {
489
+ return _resilientClient;
490
+ }
491
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
492
+ return _daemonClient;
493
+ }
479
494
  return _resilientClient;
480
495
  }
481
496
  function getRawClient() {
@@ -964,6 +979,12 @@ async function ensureSchema() {
964
979
  } catch {
965
980
  }
966
981
  }
982
+ try {
983
+ await client.execute(
984
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
985
+ );
986
+ } catch {
987
+ }
967
988
  await client.executeMultiple(`
968
989
  CREATE TABLE IF NOT EXISTS entities (
969
990
  id TEXT PRIMARY KEY,
@@ -1016,7 +1037,30 @@ async function ensureSchema() {
1016
1037
  entity_id TEXT NOT NULL,
1017
1038
  PRIMARY KEY (hyperedge_id, entity_id)
1018
1039
  );
1040
+
1041
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1042
+ name,
1043
+ content=entities,
1044
+ content_rowid=rowid
1045
+ );
1046
+
1047
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1048
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1049
+ END;
1050
+
1051
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1052
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1053
+ END;
1054
+
1055
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1056
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1057
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1058
+ END;
1019
1059
  `);
1060
+ try {
1061
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1062
+ } catch {
1063
+ }
1020
1064
  await client.executeMultiple(`
1021
1065
  CREATE TABLE IF NOT EXISTS entity_aliases (
1022
1066
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1197,6 +1241,33 @@ async function ensureSchema() {
1197
1241
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1198
1242
  ON conversations(channel_id);
1199
1243
  `);
1244
+ await client.executeMultiple(`
1245
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1246
+ session_uuid TEXT PRIMARY KEY,
1247
+ agent_id TEXT NOT NULL,
1248
+ session_name TEXT,
1249
+ task_id TEXT,
1250
+ project_name TEXT,
1251
+ started_at TEXT NOT NULL
1252
+ );
1253
+
1254
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1255
+ ON session_agent_map(agent_id);
1256
+ `);
1257
+ try {
1258
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1259
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1260
+ await client.execute({
1261
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1262
+ SELECT session_id, agent_id, '', MIN(timestamp)
1263
+ FROM memories
1264
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1265
+ GROUP BY session_id, agent_id`,
1266
+ args: []
1267
+ });
1268
+ }
1269
+ } catch {
1270
+ }
1200
1271
  try {
1201
1272
  await client.execute({
1202
1273
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1330,8 +1401,30 @@ async function ensureSchema() {
1330
1401
  });
1331
1402
  } catch {
1332
1403
  }
1404
+ for (const col of [
1405
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1406
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1407
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1408
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1409
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1410
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1411
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1412
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1413
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1414
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1415
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1416
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1417
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1418
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1419
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1420
+ ]) {
1421
+ try {
1422
+ await client.execute(col);
1423
+ } catch {
1424
+ }
1425
+ }
1333
1426
  }
1334
- var _client, _resilientClient, initTurso;
1427
+ var _client, _resilientClient, _daemonClient, initTurso;
1335
1428
  var init_database = __esm({
1336
1429
  "src/lib/database.ts"() {
1337
1430
  "use strict";
@@ -1339,6 +1432,7 @@ var init_database = __esm({
1339
1432
  init_employees();
1340
1433
  _client = null;
1341
1434
  _resilientClient = null;
1435
+ _daemonClient = null;
1342
1436
  initTurso = initDatabase;
1343
1437
  }
1344
1438
  });
@@ -2125,18 +2219,69 @@ var init_provider_table = __esm({
2125
2219
  }
2126
2220
  });
2127
2221
 
2128
- // src/lib/intercom-queue.ts
2129
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
2222
+ // src/lib/runtime-table.ts
2223
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
2224
+ var init_runtime_table = __esm({
2225
+ "src/lib/runtime-table.ts"() {
2226
+ "use strict";
2227
+ RUNTIME_TABLE = {
2228
+ codex: {
2229
+ binary: "codex",
2230
+ launchMode: "exec",
2231
+ autoApproveFlag: "--full-auto",
2232
+ inlineFlag: "--no-alt-screen",
2233
+ apiKeyEnv: "OPENAI_API_KEY",
2234
+ defaultModel: "gpt-5.4"
2235
+ }
2236
+ };
2237
+ DEFAULT_RUNTIME = "claude";
2238
+ }
2239
+ });
2240
+
2241
+ // src/lib/agent-config.ts
2242
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
2130
2243
  import path7 from "path";
2244
+ function loadAgentConfig() {
2245
+ if (!existsSync7(AGENT_CONFIG_PATH)) return {};
2246
+ try {
2247
+ return JSON.parse(readFileSync5(AGENT_CONFIG_PATH, "utf-8"));
2248
+ } catch {
2249
+ return {};
2250
+ }
2251
+ }
2252
+ function getAgentRuntime(agentId) {
2253
+ const config = loadAgentConfig();
2254
+ const entry = config[agentId];
2255
+ if (entry) return entry;
2256
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
2257
+ }
2258
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
2259
+ var init_agent_config = __esm({
2260
+ "src/lib/agent-config.ts"() {
2261
+ "use strict";
2262
+ init_config();
2263
+ init_runtime_table();
2264
+ AGENT_CONFIG_PATH = path7.join(EXE_AI_DIR, "agent-config.json");
2265
+ DEFAULT_MODELS = {
2266
+ claude: "claude-opus-4",
2267
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
2268
+ opencode: "minimax-m2.7"
2269
+ };
2270
+ }
2271
+ });
2272
+
2273
+ // src/lib/intercom-queue.ts
2274
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
2275
+ import path8 from "path";
2131
2276
  import os6 from "os";
2132
2277
  function ensureDir() {
2133
- const dir = path7.dirname(QUEUE_PATH);
2134
- if (!existsSync7(dir)) mkdirSync3(dir, { recursive: true });
2278
+ const dir = path8.dirname(QUEUE_PATH);
2279
+ if (!existsSync8(dir)) mkdirSync4(dir, { recursive: true });
2135
2280
  }
2136
2281
  function readQueue() {
2137
2282
  try {
2138
- if (!existsSync7(QUEUE_PATH)) return [];
2139
- return JSON.parse(readFileSync5(QUEUE_PATH, "utf8"));
2283
+ if (!existsSync8(QUEUE_PATH)) return [];
2284
+ return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
2140
2285
  } catch {
2141
2286
  return [];
2142
2287
  }
@@ -2144,7 +2289,7 @@ function readQueue() {
2144
2289
  function writeQueue(queue) {
2145
2290
  ensureDir();
2146
2291
  const tmp = `${QUEUE_PATH}.tmp`;
2147
- writeFileSync3(tmp, JSON.stringify(queue, null, 2));
2292
+ writeFileSync4(tmp, JSON.stringify(queue, null, 2));
2148
2293
  renameSync3(tmp, QUEUE_PATH);
2149
2294
  }
2150
2295
  function queueIntercom(targetSession, reason) {
@@ -2168,25 +2313,25 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
2168
2313
  var init_intercom_queue = __esm({
2169
2314
  "src/lib/intercom-queue.ts"() {
2170
2315
  "use strict";
2171
- QUEUE_PATH = path7.join(os6.homedir(), ".exe-os", "intercom-queue.json");
2316
+ QUEUE_PATH = path8.join(os6.homedir(), ".exe-os", "intercom-queue.json");
2172
2317
  TTL_MS = 60 * 60 * 1e3;
2173
- INTERCOM_LOG = path7.join(os6.homedir(), ".exe-os", "intercom.log");
2318
+ INTERCOM_LOG = path8.join(os6.homedir(), ".exe-os", "intercom.log");
2174
2319
  }
2175
2320
  });
2176
2321
 
2177
2322
  // src/lib/license.ts
2178
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
2323
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
2179
2324
  import { randomUUID as randomUUID2 } from "crypto";
2180
- import path8 from "path";
2325
+ import path9 from "path";
2181
2326
  import { jwtVerify, importSPKI } from "jose";
2182
2327
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
2183
2328
  var init_license = __esm({
2184
2329
  "src/lib/license.ts"() {
2185
2330
  "use strict";
2186
2331
  init_config();
2187
- LICENSE_PATH = path8.join(EXE_AI_DIR, "license.key");
2188
- CACHE_PATH = path8.join(EXE_AI_DIR, "license-cache.json");
2189
- DEVICE_ID_PATH = path8.join(EXE_AI_DIR, "device-id");
2332
+ LICENSE_PATH = path9.join(EXE_AI_DIR, "license.key");
2333
+ CACHE_PATH = path9.join(EXE_AI_DIR, "license-cache.json");
2334
+ DEVICE_ID_PATH = path9.join(EXE_AI_DIR, "device-id");
2190
2335
  PLAN_LIMITS = {
2191
2336
  free: { devices: 1, employees: 1, memories: 5e3 },
2192
2337
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -2198,12 +2343,12 @@ var init_license = __esm({
2198
2343
  });
2199
2344
 
2200
2345
  // src/lib/plan-limits.ts
2201
- import { readFileSync as readFileSync7, existsSync as existsSync9 } from "fs";
2202
- import path9 from "path";
2346
+ import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
2347
+ import path10 from "path";
2203
2348
  function getLicenseSync() {
2204
2349
  try {
2205
- if (!existsSync9(CACHE_PATH2)) return freeLicense();
2206
- const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
2350
+ if (!existsSync10(CACHE_PATH2)) return freeLicense();
2351
+ const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
2207
2352
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
2208
2353
  const parts = raw.token.split(".");
2209
2354
  if (parts.length !== 3) return freeLicense();
@@ -2241,8 +2386,8 @@ function assertEmployeeLimitSync(rosterPath) {
2241
2386
  const filePath = rosterPath ?? EMPLOYEES_PATH;
2242
2387
  let count = 0;
2243
2388
  try {
2244
- if (existsSync9(filePath)) {
2245
- const raw = readFileSync7(filePath, "utf8");
2389
+ if (existsSync10(filePath)) {
2390
+ const raw = readFileSync8(filePath, "utf8");
2246
2391
  const employees = JSON.parse(raw);
2247
2392
  count = Array.isArray(employees) ? employees.length : 0;
2248
2393
  }
@@ -2271,7 +2416,7 @@ var init_plan_limits = __esm({
2271
2416
  this.name = "PlanLimitError";
2272
2417
  }
2273
2418
  };
2274
- CACHE_PATH2 = path9.join(EXE_AI_DIR, "license-cache.json");
2419
+ CACHE_PATH2 = path10.join(EXE_AI_DIR, "license-cache.json");
2275
2420
  }
2276
2421
  });
2277
2422
 
@@ -2619,13 +2764,13 @@ __export(tmux_routing_exports, {
2619
2764
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
2620
2765
  });
2621
2766
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
2622
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync10, appendFileSync } from "fs";
2623
- import path10 from "path";
2767
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync } from "fs";
2768
+ import path11 from "path";
2624
2769
  import os7 from "os";
2625
2770
  import { fileURLToPath } from "url";
2626
2771
  import { unlinkSync as unlinkSync3 } from "fs";
2627
2772
  function spawnLockPath(sessionName) {
2628
- return path10.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
2773
+ return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
2629
2774
  }
2630
2775
  function isProcessAlive(pid) {
2631
2776
  try {
@@ -2636,13 +2781,13 @@ function isProcessAlive(pid) {
2636
2781
  }
2637
2782
  }
2638
2783
  function acquireSpawnLock(sessionName) {
2639
- if (!existsSync10(SPAWN_LOCK_DIR)) {
2640
- mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
2784
+ if (!existsSync11(SPAWN_LOCK_DIR)) {
2785
+ mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
2641
2786
  }
2642
2787
  const lockFile = spawnLockPath(sessionName);
2643
- if (existsSync10(lockFile)) {
2788
+ if (existsSync11(lockFile)) {
2644
2789
  try {
2645
- const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
2790
+ const lock = JSON.parse(readFileSync9(lockFile, "utf8"));
2646
2791
  const age = Date.now() - lock.timestamp;
2647
2792
  if (isProcessAlive(lock.pid) && age < 6e4) {
2648
2793
  return false;
@@ -2650,7 +2795,7 @@ function acquireSpawnLock(sessionName) {
2650
2795
  } catch {
2651
2796
  }
2652
2797
  }
2653
- writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
2798
+ writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
2654
2799
  return true;
2655
2800
  }
2656
2801
  function releaseSpawnLock(sessionName) {
@@ -2662,13 +2807,13 @@ function releaseSpawnLock(sessionName) {
2662
2807
  function resolveBehaviorsExporterScript() {
2663
2808
  try {
2664
2809
  const thisFile = fileURLToPath(import.meta.url);
2665
- const scriptPath = path10.join(
2666
- path10.dirname(thisFile),
2810
+ const scriptPath = path11.join(
2811
+ path11.dirname(thisFile),
2667
2812
  "..",
2668
2813
  "bin",
2669
2814
  "exe-export-behaviors.js"
2670
2815
  );
2671
- return existsSync10(scriptPath) ? scriptPath : null;
2816
+ return existsSync11(scriptPath) ? scriptPath : null;
2672
2817
  } catch {
2673
2818
  return null;
2674
2819
  }
@@ -2734,12 +2879,12 @@ function extractRootExe(name) {
2734
2879
  return parts.length > 0 ? parts[parts.length - 1] : null;
2735
2880
  }
2736
2881
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
2737
- if (!existsSync10(SESSION_CACHE)) {
2738
- mkdirSync5(SESSION_CACHE, { recursive: true });
2882
+ if (!existsSync11(SESSION_CACHE)) {
2883
+ mkdirSync6(SESSION_CACHE, { recursive: true });
2739
2884
  }
2740
2885
  const rootExe = extractRootExe(parentExe) ?? parentExe;
2741
- const filePath = path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
2742
- writeFileSync5(filePath, JSON.stringify({
2886
+ const filePath = path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
2887
+ writeFileSync6(filePath, JSON.stringify({
2743
2888
  parentExe: rootExe,
2744
2889
  dispatchedBy: dispatchedBy || rootExe,
2745
2890
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -2747,7 +2892,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
2747
2892
  }
2748
2893
  function getParentExe(sessionKey) {
2749
2894
  try {
2750
- const data = JSON.parse(readFileSync8(path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
2895
+ const data = JSON.parse(readFileSync9(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
2751
2896
  return data.parentExe || null;
2752
2897
  } catch {
2753
2898
  return null;
@@ -2755,8 +2900,8 @@ function getParentExe(sessionKey) {
2755
2900
  }
2756
2901
  function getDispatchedBy(sessionKey) {
2757
2902
  try {
2758
- const data = JSON.parse(readFileSync8(
2759
- path10.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
2903
+ const data = JSON.parse(readFileSync9(
2904
+ path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
2760
2905
  "utf8"
2761
2906
  ));
2762
2907
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -2817,32 +2962,50 @@ async function verifyPaneAtCapacity(sessionName) {
2817
2962
  }
2818
2963
  function readDebounceState() {
2819
2964
  try {
2820
- if (!existsSync10(DEBOUNCE_FILE)) return {};
2821
- return JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
2965
+ if (!existsSync11(DEBOUNCE_FILE)) return {};
2966
+ const raw = JSON.parse(readFileSync9(DEBOUNCE_FILE, "utf8"));
2967
+ const state = {};
2968
+ for (const [key, val] of Object.entries(raw)) {
2969
+ if (typeof val === "number") {
2970
+ state[key] = { lastSent: val, pending: 0 };
2971
+ } else if (val && typeof val === "object" && "lastSent" in val) {
2972
+ state[key] = val;
2973
+ }
2974
+ }
2975
+ return state;
2822
2976
  } catch {
2823
2977
  return {};
2824
2978
  }
2825
2979
  }
2826
2980
  function writeDebounceState(state) {
2827
2981
  try {
2828
- if (!existsSync10(SESSION_CACHE)) mkdirSync5(SESSION_CACHE, { recursive: true });
2829
- writeFileSync5(DEBOUNCE_FILE, JSON.stringify(state));
2982
+ if (!existsSync11(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
2983
+ writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
2830
2984
  } catch {
2831
2985
  }
2832
2986
  }
2833
2987
  function isDebounced(targetSession) {
2834
2988
  const state = readDebounceState();
2835
- const lastSent = state[targetSession] ?? 0;
2836
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
2989
+ const entry = state[targetSession];
2990
+ const lastSent = entry?.lastSent ?? 0;
2991
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
2992
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
2993
+ state[targetSession].pending++;
2994
+ writeDebounceState(state);
2995
+ return true;
2996
+ }
2997
+ return false;
2837
2998
  }
2838
2999
  function recordDebounce(targetSession) {
2839
3000
  const state = readDebounceState();
2840
- state[targetSession] = Date.now();
3001
+ const batched = state[targetSession]?.pending ?? 0;
3002
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
2841
3003
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
2842
3004
  for (const key of Object.keys(state)) {
2843
- if ((state[key] ?? 0) < cutoff) delete state[key];
3005
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
2844
3006
  }
2845
3007
  writeDebounceState(state);
3008
+ return batched;
2846
3009
  }
2847
3010
  function logIntercom(msg) {
2848
3011
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -2887,7 +3050,7 @@ function sendIntercom(targetSession) {
2887
3050
  return "skipped_exe";
2888
3051
  }
2889
3052
  if (isDebounced(targetSession)) {
2890
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
3053
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
2891
3054
  return "debounced";
2892
3055
  }
2893
3056
  try {
@@ -2899,14 +3062,14 @@ function sendIntercom(targetSession) {
2899
3062
  const sessionState = getSessionState(targetSession);
2900
3063
  if (sessionState === "no_claude") {
2901
3064
  queueIntercom(targetSession, "claude not running in session");
2902
- recordDebounce(targetSession);
2903
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
3065
+ const batched2 = recordDebounce(targetSession);
3066
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
2904
3067
  return "queued";
2905
3068
  }
2906
3069
  if (sessionState === "thinking" || sessionState === "tool") {
2907
3070
  queueIntercom(targetSession, "session busy at send time");
2908
- recordDebounce(targetSession);
2909
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
3071
+ const batched2 = recordDebounce(targetSession);
3072
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
2910
3073
  return "queued";
2911
3074
  }
2912
3075
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -2914,8 +3077,8 @@ function sendIntercom(targetSession) {
2914
3077
  transport.sendKeys(targetSession, "q");
2915
3078
  }
2916
3079
  transport.sendKeys(targetSession, "/exe-intercom");
2917
- recordDebounce(targetSession);
2918
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
3080
+ const batched = recordDebounce(targetSession);
3081
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
2919
3082
  return "delivered";
2920
3083
  } catch {
2921
3084
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -2945,7 +3108,7 @@ function notifyParentExe(sessionKey) {
2945
3108
  return true;
2946
3109
  }
2947
3110
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2948
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
3111
+ if (isCoordinatorName(employeeName)) {
2949
3112
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
2950
3113
  }
2951
3114
  try {
@@ -3017,26 +3180,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3017
3180
  const transport = getTransport();
3018
3181
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
3019
3182
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
3020
- const logDir = path10.join(os7.homedir(), ".exe-os", "session-logs");
3021
- const logFile = path10.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3022
- if (!existsSync10(logDir)) {
3023
- mkdirSync5(logDir, { recursive: true });
3183
+ const logDir = path11.join(os7.homedir(), ".exe-os", "session-logs");
3184
+ const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
3185
+ if (!existsSync11(logDir)) {
3186
+ mkdirSync6(logDir, { recursive: true });
3024
3187
  }
3025
3188
  transport.kill(sessionName);
3026
3189
  let cleanupSuffix = "";
3027
3190
  try {
3028
3191
  const thisFile = fileURLToPath(import.meta.url);
3029
- const cleanupScript = path10.join(path10.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3030
- if (existsSync10(cleanupScript)) {
3192
+ const cleanupScript = path11.join(path11.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
3193
+ if (existsSync11(cleanupScript)) {
3031
3194
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
3032
3195
  }
3033
3196
  } catch {
3034
3197
  }
3035
3198
  try {
3036
- const claudeJsonPath = path10.join(os7.homedir(), ".claude.json");
3199
+ const claudeJsonPath = path11.join(os7.homedir(), ".claude.json");
3037
3200
  let claudeJson = {};
3038
3201
  try {
3039
- claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
3202
+ claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
3040
3203
  } catch {
3041
3204
  }
3042
3205
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -3044,17 +3207,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3044
3207
  const trustDir = opts?.cwd ?? projectDir;
3045
3208
  if (!projects[trustDir]) projects[trustDir] = {};
3046
3209
  projects[trustDir].hasTrustDialogAccepted = true;
3047
- writeFileSync5(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
3210
+ writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
3048
3211
  } catch {
3049
3212
  }
3050
3213
  try {
3051
- const settingsDir = path10.join(os7.homedir(), ".claude", "projects");
3214
+ const settingsDir = path11.join(os7.homedir(), ".claude", "projects");
3052
3215
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
3053
- const projSettingsDir = path10.join(settingsDir, normalizedKey);
3054
- const settingsPath = path10.join(projSettingsDir, "settings.json");
3216
+ const projSettingsDir = path11.join(settingsDir, normalizedKey);
3217
+ const settingsPath = path11.join(projSettingsDir, "settings.json");
3055
3218
  let settings = {};
3056
3219
  try {
3057
- settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
3220
+ settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
3058
3221
  } catch {
3059
3222
  }
3060
3223
  const perms = settings.permissions ?? {};
@@ -3082,20 +3245,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3082
3245
  if (changed) {
3083
3246
  perms.allow = allow;
3084
3247
  settings.permissions = perms;
3085
- mkdirSync5(projSettingsDir, { recursive: true });
3086
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3248
+ mkdirSync6(projSettingsDir, { recursive: true });
3249
+ writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3087
3250
  }
3088
3251
  } catch {
3089
3252
  }
3090
3253
  const spawnCwd = opts?.cwd ?? projectDir;
3091
3254
  const useExeAgent = !!(opts?.model && opts?.provider);
3092
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
3255
+ const agentRtConfig = getAgentRuntime(employeeName);
3256
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
3257
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
3258
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
3093
3259
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
3094
3260
  let identityFlag = "";
3095
3261
  let behaviorsFlag = "";
3096
3262
  let legacyFallbackWarned = false;
3097
3263
  if (!useExeAgent && !useBinSymlink) {
3098
- const identityPath = path10.join(
3264
+ const identityPath = path11.join(
3099
3265
  os7.homedir(),
3100
3266
  ".exe-os",
3101
3267
  "identity",
@@ -3105,13 +3271,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3105
3271
  const hasAgentFlag = claudeSupportsAgentFlag();
3106
3272
  if (hasAgentFlag) {
3107
3273
  identityFlag = ` --agent ${employeeName}`;
3108
- } else if (existsSync10(identityPath)) {
3274
+ } else if (existsSync11(identityPath)) {
3109
3275
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
3110
3276
  legacyFallbackWarned = true;
3111
3277
  }
3112
3278
  const behaviorsFile = exportBehaviorsSync(
3113
3279
  employeeName,
3114
- path10.basename(spawnCwd),
3280
+ path11.basename(spawnCwd),
3115
3281
  sessionName
3116
3282
  );
3117
3283
  if (behaviorsFile) {
@@ -3126,16 +3292,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3126
3292
  }
3127
3293
  let sessionContextFlag = "";
3128
3294
  try {
3129
- const ctxDir = path10.join(os7.homedir(), ".exe-os", "session-cache");
3130
- mkdirSync5(ctxDir, { recursive: true });
3131
- const ctxFile = path10.join(ctxDir, `session-context-${sessionName}.md`);
3295
+ const ctxDir = path11.join(os7.homedir(), ".exe-os", "session-cache");
3296
+ mkdirSync6(ctxDir, { recursive: true });
3297
+ const ctxFile = path11.join(ctxDir, `session-context-${sessionName}.md`);
3132
3298
  const ctxContent = [
3133
3299
  `## Session Context`,
3134
3300
  `You are running in tmux session: ${sessionName}.`,
3135
3301
  `Your parent coordinator session is ${exeSession}.`,
3136
3302
  `Your employees (if any) use the -${exeSession} suffix.`
3137
3303
  ].join("\n");
3138
- writeFileSync5(ctxFile, ctxContent);
3304
+ writeFileSync6(ctxFile, ctxContent);
3139
3305
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
3140
3306
  } catch {
3141
3307
  }
@@ -3149,9 +3315,48 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3149
3315
  }
3150
3316
  }
3151
3317
  }
3318
+ if (useCodex) {
3319
+ const codexCfg = RUNTIME_TABLE.codex;
3320
+ if (codexCfg?.apiKeyEnv) {
3321
+ const keyVal = process.env[codexCfg.apiKeyEnv];
3322
+ if (keyVal) {
3323
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
3324
+ }
3325
+ }
3326
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
3327
+ }
3328
+ if (useOpencode) {
3329
+ const ocCfg = PROVIDER_TABLE.opencode;
3330
+ if (ocCfg?.apiKeyEnv) {
3331
+ const keyVal = process.env[ocCfg.apiKeyEnv];
3332
+ if (keyVal) {
3333
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
3334
+ }
3335
+ }
3336
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
3337
+ }
3338
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
3339
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
3340
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
3341
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
3342
+ }
3343
+ }
3152
3344
  let spawnCommand;
3153
3345
  if (useExeAgent) {
3154
3346
  spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
3347
+ } else if (useCodex) {
3348
+ process.stderr.write(
3349
+ `[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
3350
+ `
3351
+ );
3352
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName}${cleanupSuffix}`;
3353
+ } else if (useOpencode) {
3354
+ const binName = `${employeeName}-opencode`;
3355
+ process.stderr.write(
3356
+ `[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
3357
+ `
3358
+ );
3359
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
3155
3360
  } else if (useBinSymlink) {
3156
3361
  const binName = `${employeeName}-${ccProvider}`;
3157
3362
  process.stderr.write(
@@ -3173,11 +3378,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3173
3378
  transport.pipeLog(sessionName, logFile);
3174
3379
  try {
3175
3380
  const mySession = getMySession();
3176
- const dispatchInfo = path10.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
3177
- writeFileSync5(dispatchInfo, JSON.stringify({
3381
+ const dispatchInfo = path11.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
3382
+ writeFileSync6(dispatchInfo, JSON.stringify({
3178
3383
  dispatchedBy: mySession,
3179
3384
  rootExe: exeSession,
3180
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
3385
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
3386
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
3387
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
3181
3388
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
3182
3389
  }));
3183
3390
  } catch {
@@ -3195,6 +3402,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3195
3402
  booted = true;
3196
3403
  break;
3197
3404
  }
3405
+ } else if (useCodex) {
3406
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
3407
+ booted = true;
3408
+ break;
3409
+ }
3198
3410
  } else {
3199
3411
  if (pane.includes("Claude Code") || pane.includes("\u276F")) {
3200
3412
  booted = true;
@@ -3206,9 +3418,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3206
3418
  }
3207
3419
  if (!booted) {
3208
3420
  releaseSpawnLock(sessionName);
3209
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3421
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
3422
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
3210
3423
  }
3211
- if (!useExeAgent) {
3424
+ if (!useExeAgent && !useCodex) {
3212
3425
  try {
3213
3426
  transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
3214
3427
  } catch {
@@ -3235,17 +3448,19 @@ var init_tmux_routing = __esm({
3235
3448
  init_cc_agent_support();
3236
3449
  init_mcp_prefix();
3237
3450
  init_provider_table();
3451
+ init_agent_config();
3452
+ init_runtime_table();
3238
3453
  init_intercom_queue();
3239
3454
  init_plan_limits();
3240
3455
  init_employees();
3241
- SPAWN_LOCK_DIR = path10.join(os7.homedir(), ".exe-os", "spawn-locks");
3242
- SESSION_CACHE = path10.join(os7.homedir(), ".exe-os", "session-cache");
3456
+ SPAWN_LOCK_DIR = path11.join(os7.homedir(), ".exe-os", "spawn-locks");
3457
+ SESSION_CACHE = path11.join(os7.homedir(), ".exe-os", "session-cache");
3243
3458
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
3244
3459
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
3245
3460
  VERIFY_PANE_LINES = 200;
3246
3461
  INTERCOM_DEBOUNCE_MS = 3e4;
3247
- INTERCOM_LOG2 = path10.join(os7.homedir(), ".exe-os", "intercom.log");
3248
- DEBOUNCE_FILE = path10.join(SESSION_CACHE, "intercom-debounce.json");
3462
+ INTERCOM_LOG2 = path11.join(os7.homedir(), ".exe-os", "intercom.log");
3463
+ DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
3249
3464
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3250
3465
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
3251
3466
  }
@@ -3277,10 +3492,11 @@ var init_task_scope = __esm({
3277
3492
 
3278
3493
  // src/lib/tasks-crud.ts
3279
3494
  import crypto3 from "crypto";
3280
- import path11 from "path";
3495
+ import path12 from "path";
3496
+ import os8 from "os";
3281
3497
  import { execSync as execSync5 } from "child_process";
3282
3498
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3283
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
3499
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
3284
3500
  async function writeCheckpoint(input) {
3285
3501
  const client = getClient();
3286
3502
  const row = await resolveTask(client, input.taskId);
@@ -3321,6 +3537,35 @@ function extractParentFromContext(contextBody) {
3321
3537
  function slugify(title) {
3322
3538
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3323
3539
  }
3540
+ function buildKeywordIndex() {
3541
+ const idx = /* @__PURE__ */ new Map();
3542
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
3543
+ for (const kw of keywords) {
3544
+ const existing = idx.get(kw) ?? [];
3545
+ existing.push(role);
3546
+ idx.set(kw, existing);
3547
+ }
3548
+ }
3549
+ return idx;
3550
+ }
3551
+ function checkLaneAffinity(title, context, assigneeName) {
3552
+ const employees = loadEmployeesSync();
3553
+ const employee = employees.find((e) => e.name === assigneeName);
3554
+ if (!employee) return void 0;
3555
+ const assigneeRole = employee.role;
3556
+ const text = `${title} ${context}`.toLowerCase();
3557
+ const matchedRoles = /* @__PURE__ */ new Set();
3558
+ for (const [keyword, roles] of KEYWORD_INDEX) {
3559
+ if (text.includes(keyword)) {
3560
+ for (const role of roles) matchedRoles.add(role);
3561
+ }
3562
+ }
3563
+ if (matchedRoles.size === 0) return void 0;
3564
+ if (matchedRoles.has(assigneeRole)) return void 0;
3565
+ if (assigneeRole === "COO") return void 0;
3566
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
3567
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
3568
+ }
3324
3569
  async function resolveTask(client, identifier, scopeSession) {
3325
3570
  const scope = sessionScopeFilter(scopeSession);
3326
3571
  let result = await client.execute({
@@ -3370,7 +3615,14 @@ async function createTaskCore(input) {
3370
3615
  const id = crypto3.randomUUID();
3371
3616
  const now = (/* @__PURE__ */ new Date()).toISOString();
3372
3617
  const slug = slugify(input.title);
3373
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
3618
+ let earlySessionScope = null;
3619
+ try {
3620
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3621
+ earlySessionScope = resolveExeSession2();
3622
+ } catch {
3623
+ }
3624
+ const scope = earlySessionScope ?? "default";
3625
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
3374
3626
  let blockedById = null;
3375
3627
  const initialStatus = input.blockedBy ? "blocked" : "open";
3376
3628
  if (input.blockedBy) {
@@ -3410,22 +3662,24 @@ async function createTaskCore(input) {
3410
3662
  if (dupCheck.rows.length > 0) {
3411
3663
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
3412
3664
  }
3665
+ if (!process.env.DISABLE_LANE_AFFINITY) {
3666
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
3667
+ if (laneWarning) {
3668
+ warning = warning ? `${warning}
3669
+ ${laneWarning}` : laneWarning;
3670
+ }
3671
+ }
3413
3672
  if (input.baseDir) {
3414
3673
  try {
3415
- await mkdir4(path11.join(input.baseDir, "exe", "output"), { recursive: true });
3416
- await mkdir4(path11.join(input.baseDir, "exe", "research"), { recursive: true });
3674
+ await mkdir4(path12.join(input.baseDir, "exe", "output"), { recursive: true });
3675
+ await mkdir4(path12.join(input.baseDir, "exe", "research"), { recursive: true });
3417
3676
  await ensureArchitectureDoc(input.baseDir, input.projectName);
3418
3677
  await ensureGitignoreExe(input.baseDir);
3419
3678
  } catch {
3420
3679
  }
3421
3680
  }
3422
3681
  const complexity = input.complexity ?? "standard";
3423
- let sessionScope = null;
3424
- try {
3425
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
3426
- sessionScope = resolveExeSession2();
3427
- } catch {
3428
- }
3682
+ const sessionScope = earlySessionScope;
3429
3683
  await client.execute({
3430
3684
  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)
3431
3685
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -3452,6 +3706,43 @@ async function createTaskCore(input) {
3452
3706
  now
3453
3707
  ]
3454
3708
  });
3709
+ if (input.baseDir) {
3710
+ try {
3711
+ const EXE_OS_DIR = path12.join(os8.homedir(), ".exe-os");
3712
+ const mdPath = path12.join(EXE_OS_DIR, taskFile);
3713
+ const mdDir = path12.dirname(mdPath);
3714
+ if (!existsSync12(mdDir)) await mkdir4(mdDir, { recursive: true });
3715
+ const reviewer = input.reviewer ?? input.assignedBy;
3716
+ const mdContent = `# ${input.title}
3717
+
3718
+ **ID:** ${id}
3719
+ **Status:** ${initialStatus}
3720
+ **Priority:** ${input.priority}
3721
+ **Assigned by:** ${input.assignedBy}
3722
+ **Assigned to:** ${input.assignedTo}
3723
+ **Project:** ${input.projectName}
3724
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
3725
+ **Parent task:** ${parentTaskId}` : ""}
3726
+ **Reviewer:** ${reviewer}
3727
+
3728
+ ## Context
3729
+
3730
+ ${input.context}
3731
+
3732
+ ## MANDATORY: When done
3733
+
3734
+ You MUST call update_task with status "done" and a result summary when finished.
3735
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
3736
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
3737
+ `;
3738
+ await writeFile4(mdPath, mdContent, "utf-8");
3739
+ } catch (err) {
3740
+ process.stderr.write(
3741
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
3742
+ `
3743
+ );
3744
+ }
3745
+ }
3455
3746
  return {
3456
3747
  id,
3457
3748
  title: input.title,
@@ -3644,7 +3935,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
3644
3935
  return { row, taskFile, now, taskId };
3645
3936
  }
3646
3937
  }
3647
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
3938
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
3648
3939
  process.stderr.write(
3649
3940
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
3650
3941
  `
@@ -3709,9 +4000,9 @@ async function deleteTaskCore(taskId, _baseDir) {
3709
4000
  return { taskFile, assignedTo, assignedBy, taskSlug };
3710
4001
  }
3711
4002
  async function ensureArchitectureDoc(baseDir, projectName) {
3712
- const archPath = path11.join(baseDir, "exe", "ARCHITECTURE.md");
4003
+ const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
3713
4004
  try {
3714
- if (existsSync11(archPath)) return;
4005
+ if (existsSync12(archPath)) return;
3715
4006
  const template = [
3716
4007
  `# ${projectName} \u2014 System Architecture`,
3717
4008
  "",
@@ -3744,10 +4035,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
3744
4035
  }
3745
4036
  }
3746
4037
  async function ensureGitignoreExe(baseDir) {
3747
- const gitignorePath = path11.join(baseDir, ".gitignore");
4038
+ const gitignorePath = path12.join(baseDir, ".gitignore");
3748
4039
  try {
3749
- if (existsSync11(gitignorePath)) {
3750
- const content = readFileSync9(gitignorePath, "utf-8");
4040
+ if (existsSync12(gitignorePath)) {
4041
+ const content = readFileSync10(gitignorePath, "utf-8");
3751
4042
  if (/^\/?exe\/?$/m.test(content)) return;
3752
4043
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
3753
4044
  } else {
@@ -3756,20 +4047,30 @@ async function ensureGitignoreExe(baseDir) {
3756
4047
  } catch {
3757
4048
  }
3758
4049
  }
3759
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
4050
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
3760
4051
  var init_tasks_crud = __esm({
3761
4052
  "src/lib/tasks-crud.ts"() {
3762
4053
  "use strict";
3763
4054
  init_database();
3764
4055
  init_task_scope();
4056
+ init_employees();
4057
+ LANE_KEYWORDS = {
4058
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
4059
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
4060
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
4061
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
4062
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
4063
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
4064
+ };
4065
+ KEYWORD_INDEX = buildKeywordIndex();
3765
4066
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
3766
4067
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
3767
4068
  }
3768
4069
  });
3769
4070
 
3770
4071
  // src/lib/tasks-review.ts
3771
- import path12 from "path";
3772
- import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
4072
+ import path13 from "path";
4073
+ import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
3773
4074
  async function countPendingReviews(sessionScope) {
3774
4075
  const client = getClient();
3775
4076
  if (sessionScope) {
@@ -3791,7 +4092,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3791
4092
  const result2 = await client.execute({
3792
4093
  sql: `SELECT COUNT(*) as cnt FROM tasks
3793
4094
  WHERE status = 'needs_review' AND updated_at > ?
3794
- AND (session_scope = ? OR session_scope IS NULL)`,
4095
+ AND session_scope = ?`,
3795
4096
  args: [sinceIso, sessionScope]
3796
4097
  });
3797
4098
  return Number(result2.rows[0]?.cnt) || 0;
@@ -3809,7 +4110,7 @@ async function listPendingReviews(limit, sessionScope) {
3809
4110
  const result2 = await client.execute({
3810
4111
  sql: `SELECT title, assigned_to, project_name FROM tasks
3811
4112
  WHERE status = 'needs_review'
3812
- AND (session_scope = ? OR session_scope IS NULL)
4113
+ AND session_scope = ?
3813
4114
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
3814
4115
  args: [sessionScope, limit]
3815
4116
  });
@@ -3930,14 +4231,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3930
4231
  if (parts.length >= 3 && parts[0] === "review") {
3931
4232
  const agent = parts[1];
3932
4233
  const slug = parts.slice(2).join("-");
3933
- const originalTaskFile = `exe/${agent}/${slug}.md`;
4234
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
3934
4235
  const result = await client.execute({
3935
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
3936
- args: [now, originalTaskFile]
4236
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
4237
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
3937
4238
  });
3938
4239
  if (result.rowsAffected > 0) {
3939
4240
  process.stderr.write(
3940
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
4241
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
3941
4242
  `
3942
4243
  );
3943
4244
  }
@@ -3950,11 +4251,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3950
4251
  );
3951
4252
  }
3952
4253
  try {
3953
- const cacheDir = path12.join(EXE_AI_DIR, "session-cache");
3954
- if (existsSync12(cacheDir)) {
4254
+ const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
4255
+ if (existsSync13(cacheDir)) {
3955
4256
  for (const f of readdirSync3(cacheDir)) {
3956
4257
  if (f.startsWith("review-notified-")) {
3957
- unlinkSync4(path12.join(cacheDir, f));
4258
+ unlinkSync4(path13.join(cacheDir, f));
3958
4259
  }
3959
4260
  }
3960
4261
  }
@@ -3975,7 +4276,7 @@ var init_tasks_review = __esm({
3975
4276
  });
3976
4277
 
3977
4278
  // src/lib/tasks-chain.ts
3978
- import path13 from "path";
4279
+ import path14 from "path";
3979
4280
  import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
3980
4281
  async function cascadeUnblock(taskId, baseDir, now) {
3981
4282
  const client = getClient();
@@ -3992,7 +4293,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
3992
4293
  });
3993
4294
  for (const ur of unblockedRows.rows) {
3994
4295
  try {
3995
- const ubFile = path13.join(baseDir, String(ur.task_file));
4296
+ const ubFile = path14.join(baseDir, String(ur.task_file));
3996
4297
  let ubContent = await readFile4(ubFile, "utf-8");
3997
4298
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
3998
4299
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -4061,7 +4362,7 @@ var init_tasks_chain = __esm({
4061
4362
 
4062
4363
  // src/lib/project-name.ts
4063
4364
  import { execSync as execSync6 } from "child_process";
4064
- import path14 from "path";
4365
+ import path15 from "path";
4065
4366
  function getProjectName(cwd) {
4066
4367
  const dir = cwd ?? process.cwd();
4067
4368
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -4074,7 +4375,7 @@ function getProjectName(cwd) {
4074
4375
  timeout: 2e3,
4075
4376
  stdio: ["pipe", "pipe", "pipe"]
4076
4377
  }).trim();
4077
- repoRoot = path14.dirname(gitCommonDir);
4378
+ repoRoot = path15.dirname(gitCommonDir);
4078
4379
  } catch {
4079
4380
  repoRoot = execSync6("git rev-parse --show-toplevel", {
4080
4381
  cwd: dir,
@@ -4083,11 +4384,11 @@ function getProjectName(cwd) {
4083
4384
  stdio: ["pipe", "pipe", "pipe"]
4084
4385
  }).trim();
4085
4386
  }
4086
- _cached2 = path14.basename(repoRoot);
4387
+ _cached2 = path15.basename(repoRoot);
4087
4388
  _cachedCwd = dir;
4088
4389
  return _cached2;
4089
4390
  } catch {
4090
- _cached2 = path14.basename(dir);
4391
+ _cached2 = path15.basename(dir);
4091
4392
  _cachedCwd = dir;
4092
4393
  return _cached2;
4093
4394
  }
@@ -4119,7 +4420,7 @@ function findSessionForProject(projectName) {
4119
4420
  const sessions = listSessions();
4120
4421
  for (const s of sessions) {
4121
4422
  const proj = s.projectDir.split("/").filter(Boolean).pop();
4122
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
4423
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
4123
4424
  }
4124
4425
  return null;
4125
4426
  }
@@ -4165,7 +4466,7 @@ var init_session_scope = __esm({
4165
4466
 
4166
4467
  // src/lib/tasks-notify.ts
4167
4468
  async function dispatchTaskToEmployee(input) {
4168
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
4469
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
4169
4470
  let crossProject = false;
4170
4471
  if (input.projectName) {
4171
4472
  try {
@@ -4560,8 +4861,8 @@ __export(tasks_exports, {
4560
4861
  updateTaskStatus: () => updateTaskStatus,
4561
4862
  writeCheckpoint: () => writeCheckpoint
4562
4863
  });
4563
- import path15 from "path";
4564
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
4864
+ import path16 from "path";
4865
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
4565
4866
  async function createTask(input) {
4566
4867
  const result = await createTaskCore(input);
4567
4868
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -4580,11 +4881,11 @@ async function updateTask(input) {
4580
4881
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
4581
4882
  try {
4582
4883
  const agent = String(row.assigned_to);
4583
- const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
4584
- const cachePath = path15.join(cacheDir, `current-task-${agent}.json`);
4884
+ const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
4885
+ const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
4585
4886
  if (input.status === "in_progress") {
4586
- mkdirSync6(cacheDir, { recursive: true });
4587
- writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4887
+ mkdirSync7(cacheDir, { recursive: true });
4888
+ writeFileSync7(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
4588
4889
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
4589
4890
  try {
4590
4891
  unlinkSync5(cachePath);
@@ -4644,7 +4945,7 @@ async function updateTask(input) {
4644
4945
  }
4645
4946
  const isTerminal = input.status === "done" || input.status === "needs_review";
4646
4947
  if (isTerminal) {
4647
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
4948
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
4648
4949
  if (!isCoordinator) {
4649
4950
  notifyTaskDone();
4650
4951
  }
@@ -4669,7 +4970,7 @@ async function updateTask(input) {
4669
4970
  }
4670
4971
  }
4671
4972
  }
4672
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4973
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
4673
4974
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
4674
4975
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
4675
4976
  taskId,
@@ -4685,7 +4986,7 @@ async function updateTask(input) {
4685
4986
  });
4686
4987
  }
4687
4988
  let nextTask;
4688
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
4989
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
4689
4990
  try {
4690
4991
  nextTask = await findNextTask(String(row.assigned_to));
4691
4992
  } catch {
@@ -4742,6 +5043,7 @@ var init_tasks = __esm({
4742
5043
  });
4743
5044
 
4744
5045
  // src/lib/store.ts
5046
+ import { createHash } from "crypto";
4745
5047
  init_database();
4746
5048
 
4747
5049
  // src/lib/keychain.ts
@@ -4777,12 +5079,20 @@ async function getMasterKey() {
4777
5079
  }
4778
5080
  const keyPath = getKeyPath();
4779
5081
  if (!existsSync3(keyPath)) {
5082
+ process.stderr.write(
5083
+ `[keychain] Key not found at ${keyPath} (HOME=${os3.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
5084
+ `
5085
+ );
4780
5086
  return null;
4781
5087
  }
4782
5088
  try {
4783
5089
  const content = await readFile3(keyPath, "utf-8");
4784
5090
  return Buffer.from(content.trim(), "base64");
4785
- } catch {
5091
+ } catch (err) {
5092
+ process.stderr.write(
5093
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
5094
+ `
5095
+ );
4786
5096
  return null;
4787
5097
  }
4788
5098
  }