@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
@@ -632,6 +632,7 @@ __export(employees_exports, {
632
632
  DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
633
633
  EMPLOYEES_PATH: () => EMPLOYEES_PATH,
634
634
  addEmployee: () => addEmployee,
635
+ baseAgentName: () => baseAgentName,
635
636
  canCoordinate: () => canCoordinate,
636
637
  getCoordinatorEmployee: () => getCoordinatorEmployee,
637
638
  getCoordinatorName: () => getCoordinatorName,
@@ -728,6 +729,14 @@ function hasRole(agentName, role) {
728
729
  const emp = getEmployee(employees, agentName);
729
730
  return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
730
731
  }
732
+ function baseAgentName(name, employees) {
733
+ const match = name.match(/^([a-zA-Z]+)\d+$/);
734
+ if (!match) return name;
735
+ const base = match[1];
736
+ const roster = employees ?? loadEmployeesSync();
737
+ if (getEmployee(roster, base)) return base;
738
+ return name;
739
+ }
731
740
  function isMultiInstance(agentName, employees) {
732
741
  const roster = employees ?? loadEmployeesSync();
733
742
  const emp = getEmployee(roster, agentName);
@@ -844,6 +853,12 @@ function getClient() {
844
853
  if (!_resilientClient) {
845
854
  throw new Error("Database client not initialized. Call initDatabase() first.");
846
855
  }
856
+ if (process.env.EXE_IS_DAEMON === "1") {
857
+ return _resilientClient;
858
+ }
859
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
860
+ return _daemonClient;
861
+ }
847
862
  return _resilientClient;
848
863
  }
849
864
  function getRawClient() {
@@ -1332,6 +1347,12 @@ async function ensureSchema() {
1332
1347
  } catch {
1333
1348
  }
1334
1349
  }
1350
+ try {
1351
+ await client.execute(
1352
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1353
+ );
1354
+ } catch {
1355
+ }
1335
1356
  await client.executeMultiple(`
1336
1357
  CREATE TABLE IF NOT EXISTS entities (
1337
1358
  id TEXT PRIMARY KEY,
@@ -1384,7 +1405,30 @@ async function ensureSchema() {
1384
1405
  entity_id TEXT NOT NULL,
1385
1406
  PRIMARY KEY (hyperedge_id, entity_id)
1386
1407
  );
1408
+
1409
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1410
+ name,
1411
+ content=entities,
1412
+ content_rowid=rowid
1413
+ );
1414
+
1415
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1416
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1417
+ END;
1418
+
1419
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1420
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1421
+ END;
1422
+
1423
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1424
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1425
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1426
+ END;
1387
1427
  `);
1428
+ try {
1429
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1430
+ } catch {
1431
+ }
1388
1432
  await client.executeMultiple(`
1389
1433
  CREATE TABLE IF NOT EXISTS entity_aliases (
1390
1434
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1565,6 +1609,33 @@ async function ensureSchema() {
1565
1609
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1566
1610
  ON conversations(channel_id);
1567
1611
  `);
1612
+ await client.executeMultiple(`
1613
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1614
+ session_uuid TEXT PRIMARY KEY,
1615
+ agent_id TEXT NOT NULL,
1616
+ session_name TEXT,
1617
+ task_id TEXT,
1618
+ project_name TEXT,
1619
+ started_at TEXT NOT NULL
1620
+ );
1621
+
1622
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1623
+ ON session_agent_map(agent_id);
1624
+ `);
1625
+ try {
1626
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1627
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1628
+ await client.execute({
1629
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1630
+ SELECT session_id, agent_id, '', MIN(timestamp)
1631
+ FROM memories
1632
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1633
+ GROUP BY session_id, agent_id`,
1634
+ args: []
1635
+ });
1636
+ }
1637
+ } catch {
1638
+ }
1568
1639
  try {
1569
1640
  await client.execute({
1570
1641
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1698,15 +1769,41 @@ async function ensureSchema() {
1698
1769
  });
1699
1770
  } catch {
1700
1771
  }
1772
+ for (const col of [
1773
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1774
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1775
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1776
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1777
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1778
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1779
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1780
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1781
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1782
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1783
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1784
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1785
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1786
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1787
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1788
+ ]) {
1789
+ try {
1790
+ await client.execute(col);
1791
+ } catch {
1792
+ }
1793
+ }
1701
1794
  }
1702
1795
  async function disposeDatabase() {
1796
+ if (_daemonClient) {
1797
+ _daemonClient.close();
1798
+ _daemonClient = null;
1799
+ }
1703
1800
  if (_client) {
1704
1801
  _client.close();
1705
1802
  _client = null;
1706
1803
  _resilientClient = null;
1707
1804
  }
1708
1805
  }
1709
- var _client, _resilientClient, initTurso, disposeTurso;
1806
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1710
1807
  var init_database = __esm({
1711
1808
  "src/lib/database.ts"() {
1712
1809
  "use strict";
@@ -1714,6 +1811,7 @@ var init_database = __esm({
1714
1811
  init_employees();
1715
1812
  _client = null;
1716
1813
  _resilientClient = null;
1814
+ _daemonClient = null;
1717
1815
  initTurso = initDatabase;
1718
1816
  disposeTurso = disposeDatabase;
1719
1817
  }
@@ -1748,10 +1846,12 @@ function handleData(chunk) {
1748
1846
  if (!line) continue;
1749
1847
  try {
1750
1848
  const response = JSON.parse(line);
1751
- const entry = _pending.get(response.id);
1849
+ const id = response.id;
1850
+ if (!id) continue;
1851
+ const entry = _pending.get(id);
1752
1852
  if (entry) {
1753
1853
  clearTimeout(entry.timer);
1754
- _pending.delete(response.id);
1854
+ _pending.delete(id);
1755
1855
  entry.resolve(response);
1756
1856
  }
1757
1857
  } catch {
@@ -1922,6 +2022,9 @@ async function connectEmbedDaemon() {
1922
2022
  return false;
1923
2023
  }
1924
2024
  function sendRequest(texts, priority) {
2025
+ return sendDaemonRequest({ texts, priority });
2026
+ }
2027
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1925
2028
  return new Promise((resolve) => {
1926
2029
  if (!_socket || !_connected) {
1927
2030
  resolve({ error: "Not connected" });
@@ -1931,10 +2034,10 @@ function sendRequest(texts, priority) {
1931
2034
  const timer = setTimeout(() => {
1932
2035
  _pending.delete(id);
1933
2036
  resolve({ error: "Request timeout" });
1934
- }, REQUEST_TIMEOUT_MS);
2037
+ }, timeoutMs);
1935
2038
  _pending.set(id, { resolve, timer });
1936
2039
  try {
1937
- _socket.write(JSON.stringify({ id, texts, priority }) + "\n");
2040
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
1938
2041
  } catch {
1939
2042
  clearTimeout(timer);
1940
2043
  _pending.delete(id);
@@ -1944,30 +2047,11 @@ function sendRequest(texts, priority) {
1944
2047
  }
1945
2048
  async function pingDaemon() {
1946
2049
  if (!_socket || !_connected) return null;
1947
- return new Promise((resolve) => {
1948
- const id = randomUUID();
1949
- const timer = setTimeout(() => {
1950
- _pending.delete(id);
1951
- resolve(null);
1952
- }, 5e3);
1953
- _pending.set(id, {
1954
- resolve: (data) => {
1955
- if (data.health) {
1956
- resolve(data.health);
1957
- } else {
1958
- resolve(null);
1959
- }
1960
- },
1961
- timer
1962
- });
1963
- try {
1964
- _socket.write(JSON.stringify({ id, type: "health" }) + "\n");
1965
- } catch {
1966
- clearTimeout(timer);
1967
- _pending.delete(id);
1968
- resolve(null);
1969
- }
1970
- });
2050
+ const response = await sendDaemonRequest({ type: "health" }, 5e3);
2051
+ if (response.health) {
2052
+ return response.health;
2053
+ }
2054
+ return null;
1971
2055
  }
1972
2056
  function killAndRespawnDaemon() {
1973
2057
  process.stderr.write("[exed-client] Killing daemon for restart...\n");
@@ -2110,10 +2194,10 @@ async function disposeEmbedder() {
2110
2194
  async function embedDirect(text) {
2111
2195
  const llamaCpp = await import("node-llama-cpp");
2112
2196
  const { MODELS_DIR: MODELS_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
2113
- const { existsSync: existsSync15 } = await import("fs");
2114
- const path19 = await import("path");
2115
- const modelPath = path19.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
2116
- if (!existsSync15(modelPath)) {
2197
+ const { existsSync: existsSync16 } = await import("fs");
2198
+ const path20 = await import("path");
2199
+ const modelPath = path20.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
2200
+ if (!existsSync16(modelPath)) {
2117
2201
  throw new Error(`Embedding model not found at ${modelPath}. Run '/exe-setup' to download it.`);
2118
2202
  }
2119
2203
  const llama = await llamaCpp.getLlama();
@@ -2172,12 +2256,20 @@ async function getMasterKey() {
2172
2256
  }
2173
2257
  const keyPath = getKeyPath();
2174
2258
  if (!existsSync4(keyPath)) {
2259
+ process.stderr.write(
2260
+ `[keychain] Key not found at ${keyPath} (HOME=${os3.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
2261
+ `
2262
+ );
2175
2263
  return null;
2176
2264
  }
2177
2265
  try {
2178
2266
  const content = await readFile3(keyPath, "utf-8");
2179
2267
  return Buffer.from(content.trim(), "base64");
2180
- } catch {
2268
+ } catch (err) {
2269
+ process.stderr.write(
2270
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
2271
+ `
2272
+ );
2181
2273
  return null;
2182
2274
  }
2183
2275
  }
@@ -2633,6 +2725,7 @@ __export(store_exports, {
2633
2725
  vectorToBlob: () => vectorToBlob,
2634
2726
  writeMemory: () => writeMemory
2635
2727
  });
2728
+ import { createHash } from "crypto";
2636
2729
  function isBusyError2(err) {
2637
2730
  if (err instanceof Error) {
2638
2731
  const msg = err.message.toLowerCase();
@@ -2706,12 +2799,52 @@ function classifyTier(record) {
2706
2799
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
2707
2800
  return 3;
2708
2801
  }
2802
+ function inferFilePaths(record) {
2803
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
2804
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
2805
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
2806
+ return match ? JSON.stringify([match[1]]) : null;
2807
+ }
2808
+ function inferCommitHash(record) {
2809
+ if (record.tool_name !== "Bash") return null;
2810
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
2811
+ return match ? match[1] : null;
2812
+ }
2813
+ function inferLanguageType(record) {
2814
+ const text = record.raw_text;
2815
+ if (!text || text.length < 10) return null;
2816
+ const trimmed = text.trimStart();
2817
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
2818
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
2819
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
2820
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
2821
+ return "mixed";
2822
+ }
2823
+ function inferDomain(record) {
2824
+ const proj = (record.project_name ?? "").toLowerCase();
2825
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
2826
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
2827
+ return null;
2828
+ }
2709
2829
  async function writeMemory(record) {
2710
2830
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
2711
2831
  throw new Error(
2712
2832
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
2713
2833
  );
2714
2834
  }
2835
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
2836
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
2837
+ return;
2838
+ }
2839
+ try {
2840
+ const client = getClient();
2841
+ const existing = await client.execute({
2842
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
2843
+ args: [contentHash, record.agent_id]
2844
+ });
2845
+ if (existing.rows.length > 0) return;
2846
+ } catch {
2847
+ }
2715
2848
  const dbRow = {
2716
2849
  id: record.id,
2717
2850
  agent_id: record.agent_id,
@@ -2741,7 +2874,23 @@ async function writeMemory(record) {
2741
2874
  supersedes_id: record.supersedes_id ?? null,
2742
2875
  draft: record.draft ? 1 : 0,
2743
2876
  memory_type: record.memory_type ?? "raw",
2744
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
2877
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
2878
+ content_hash: contentHash,
2879
+ intent: record.intent ?? null,
2880
+ outcome: record.outcome ?? null,
2881
+ domain: record.domain ?? inferDomain(record),
2882
+ referenced_entities: record.referenced_entities ?? null,
2883
+ retrieval_count: record.retrieval_count ?? 0,
2884
+ chain_position: record.chain_position ?? null,
2885
+ review_status: record.review_status ?? null,
2886
+ context_window_pct: record.context_window_pct ?? null,
2887
+ file_paths: record.file_paths ?? inferFilePaths(record),
2888
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
2889
+ duration_ms: record.duration_ms ?? null,
2890
+ token_cost: record.token_cost ?? null,
2891
+ audience: record.audience ?? null,
2892
+ language_type: record.language_type ?? inferLanguageType(record),
2893
+ parent_memory_id: record.parent_memory_id ?? null
2745
2894
  };
2746
2895
  _pendingRecords.push(dbRow);
2747
2896
  orgBus.emit({
@@ -2799,80 +2948,85 @@ async function flushBatch() {
2799
2948
  const draft = row.draft ? 1 : 0;
2800
2949
  const memoryType = row.memory_type ?? "raw";
2801
2950
  const trajectory = row.trajectory ?? null;
2802
- return {
2803
- sql: hasVector ? `INSERT OR IGNORE INTO memories
2804
- (id, agent_id, agent_role, session_id, timestamp,
2805
- tool_name, project_name,
2806
- has_error, raw_text, vector, version, task_id, importance, status,
2807
- confidence, last_accessed,
2808
- workspace_id, document_id, user_id, char_offset, page_number,
2809
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2810
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
2811
- (id, agent_id, agent_role, session_id, timestamp,
2951
+ const contentHash = row.content_hash ?? null;
2952
+ const intent = row.intent ?? null;
2953
+ const outcome = row.outcome ?? null;
2954
+ const domain = row.domain ?? null;
2955
+ const referencedEntities = row.referenced_entities ?? null;
2956
+ const retrievalCount = row.retrieval_count ?? 0;
2957
+ const chainPosition = row.chain_position ?? null;
2958
+ const reviewStatus = row.review_status ?? null;
2959
+ const contextWindowPct = row.context_window_pct ?? null;
2960
+ const filePaths = row.file_paths ?? null;
2961
+ const commitHash = row.commit_hash ?? null;
2962
+ const durationMs = row.duration_ms ?? null;
2963
+ const tokenCost = row.token_cost ?? null;
2964
+ const audience = row.audience ?? null;
2965
+ const languageType = row.language_type ?? null;
2966
+ const parentMemoryId = row.parent_memory_id ?? null;
2967
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
2812
2968
  tool_name, project_name,
2813
2969
  has_error, raw_text, vector, version, task_id, importance, status,
2814
2970
  confidence, last_accessed,
2815
2971
  workspace_id, document_id, user_id, char_offset, page_number,
2816
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2817
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2818
- args: hasVector ? [
2819
- row.id,
2820
- row.agent_id,
2821
- row.agent_role,
2822
- row.session_id,
2823
- row.timestamp,
2824
- row.tool_name,
2825
- row.project_name,
2826
- row.has_error,
2827
- row.raw_text,
2828
- vectorToBlob(row.vector),
2829
- row.version,
2830
- taskId,
2831
- importance,
2832
- status,
2833
- confidence,
2834
- lastAccessed,
2835
- workspaceId,
2836
- documentId,
2837
- userId,
2838
- charOffset,
2839
- pageNumber,
2840
- sourcePath,
2841
- sourceType,
2842
- tier,
2843
- supersedesId,
2844
- draft,
2845
- memoryType,
2846
- trajectory
2847
- ] : [
2848
- row.id,
2849
- row.agent_id,
2850
- row.agent_role,
2851
- row.session_id,
2852
- row.timestamp,
2853
- row.tool_name,
2854
- row.project_name,
2855
- row.has_error,
2856
- row.raw_text,
2857
- row.version,
2858
- taskId,
2859
- importance,
2860
- status,
2861
- confidence,
2862
- lastAccessed,
2863
- workspaceId,
2864
- documentId,
2865
- userId,
2866
- charOffset,
2867
- pageNumber,
2868
- sourcePath,
2869
- sourceType,
2870
- tier,
2871
- supersedesId,
2872
- draft,
2873
- memoryType,
2874
- trajectory
2875
- ]
2972
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
2973
+ intent, outcome, domain, referenced_entities, retrieval_count,
2974
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
2975
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
2976
+ const metaArgs = [
2977
+ intent,
2978
+ outcome,
2979
+ domain,
2980
+ referencedEntities,
2981
+ retrievalCount,
2982
+ chainPosition,
2983
+ reviewStatus,
2984
+ contextWindowPct,
2985
+ filePaths,
2986
+ commitHash,
2987
+ durationMs,
2988
+ tokenCost,
2989
+ audience,
2990
+ languageType,
2991
+ parentMemoryId
2992
+ ];
2993
+ const baseArgs = [
2994
+ row.id,
2995
+ row.agent_id,
2996
+ row.agent_role,
2997
+ row.session_id,
2998
+ row.timestamp,
2999
+ row.tool_name,
3000
+ row.project_name,
3001
+ row.has_error,
3002
+ row.raw_text
3003
+ ];
3004
+ const sharedArgs = [
3005
+ row.version,
3006
+ taskId,
3007
+ importance,
3008
+ status,
3009
+ confidence,
3010
+ lastAccessed,
3011
+ workspaceId,
3012
+ documentId,
3013
+ userId,
3014
+ charOffset,
3015
+ pageNumber,
3016
+ sourcePath,
3017
+ sourceType,
3018
+ tier,
3019
+ supersedesId,
3020
+ draft,
3021
+ memoryType,
3022
+ trajectory,
3023
+ contentHash
3024
+ ];
3025
+ return {
3026
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
3027
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
3028
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3029
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
2876
3030
  };
2877
3031
  };
2878
3032
  const globalClient = getClient();
@@ -3131,8 +3285,8 @@ __export(wiki_client_exports, {
3131
3285
  listDocuments: () => listDocuments,
3132
3286
  listWorkspaces: () => listWorkspaces
3133
3287
  });
3134
- async function wikiFetch(config2, path19, method = "GET", body) {
3135
- const url = `${config2.baseUrl}/api/v1${path19}`;
3288
+ async function wikiFetch(config2, path20, method = "GET", body) {
3289
+ const url = `${config2.baseUrl}/api/v1${path20}`;
3136
3290
  const headers = {
3137
3291
  Authorization: `Bearer ${config2.apiKey}`,
3138
3292
  "Content-Type": "application/json"
@@ -3165,7 +3319,7 @@ async function wikiFetch(config2, path19, method = "GET", body) {
3165
3319
  }
3166
3320
  }
3167
3321
  if (!response.ok) {
3168
- throw new Error(`Wiki API ${method} ${path19}: ${response.status} ${response.statusText}`);
3322
+ throw new Error(`Wiki API ${method} ${path20}: ${response.status} ${response.statusText}`);
3169
3323
  }
3170
3324
  return response.json();
3171
3325
  } finally {
@@ -3259,7 +3413,7 @@ var LOCAL_WIKI_URL, REQUEST_TIMEOUT_MS2;
3259
3413
  var init_wiki_client = __esm({
3260
3414
  "src/lib/wiki-client.ts"() {
3261
3415
  "use strict";
3262
- LOCAL_WIKI_URL = "http://localhost:3001";
3416
+ LOCAL_WIKI_URL = process.env.EXE_WIKI_URL || "http://localhost:3001";
3263
3417
  REQUEST_TIMEOUT_MS2 = 8e3;
3264
3418
  }
3265
3419
  });
@@ -4480,18 +4634,69 @@ var init_provider_table = __esm({
4480
4634
  }
4481
4635
  });
4482
4636
 
4483
- // src/lib/intercom-queue.ts
4484
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, renameSync as renameSync3, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
4637
+ // src/lib/runtime-table.ts
4638
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
4639
+ var init_runtime_table = __esm({
4640
+ "src/lib/runtime-table.ts"() {
4641
+ "use strict";
4642
+ RUNTIME_TABLE = {
4643
+ codex: {
4644
+ binary: "codex",
4645
+ launchMode: "exec",
4646
+ autoApproveFlag: "--full-auto",
4647
+ inlineFlag: "--no-alt-screen",
4648
+ apiKeyEnv: "OPENAI_API_KEY",
4649
+ defaultModel: "gpt-5.4"
4650
+ }
4651
+ };
4652
+ DEFAULT_RUNTIME = "claude";
4653
+ }
4654
+ });
4655
+
4656
+ // src/lib/agent-config.ts
4657
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
4485
4658
  import path8 from "path";
4659
+ function loadAgentConfig() {
4660
+ if (!existsSync7(AGENT_CONFIG_PATH)) return {};
4661
+ try {
4662
+ return JSON.parse(readFileSync6(AGENT_CONFIG_PATH, "utf-8"));
4663
+ } catch {
4664
+ return {};
4665
+ }
4666
+ }
4667
+ function getAgentRuntime(agentId) {
4668
+ const config2 = loadAgentConfig();
4669
+ const entry = config2[agentId];
4670
+ if (entry) return entry;
4671
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
4672
+ }
4673
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
4674
+ var init_agent_config = __esm({
4675
+ "src/lib/agent-config.ts"() {
4676
+ "use strict";
4677
+ init_config();
4678
+ init_runtime_table();
4679
+ AGENT_CONFIG_PATH = path8.join(EXE_AI_DIR, "agent-config.json");
4680
+ DEFAULT_MODELS = {
4681
+ claude: "claude-opus-4",
4682
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
4683
+ opencode: "minimax-m2.7"
4684
+ };
4685
+ }
4686
+ });
4687
+
4688
+ // src/lib/intercom-queue.ts
4689
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
4690
+ import path9 from "path";
4486
4691
  import os6 from "os";
4487
4692
  function ensureDir() {
4488
- const dir = path8.dirname(QUEUE_PATH);
4489
- if (!existsSync7(dir)) mkdirSync4(dir, { recursive: true });
4693
+ const dir = path9.dirname(QUEUE_PATH);
4694
+ if (!existsSync8(dir)) mkdirSync5(dir, { recursive: true });
4490
4695
  }
4491
4696
  function readQueue() {
4492
4697
  try {
4493
- if (!existsSync7(QUEUE_PATH)) return [];
4494
- return JSON.parse(readFileSync6(QUEUE_PATH, "utf8"));
4698
+ if (!existsSync8(QUEUE_PATH)) return [];
4699
+ return JSON.parse(readFileSync7(QUEUE_PATH, "utf8"));
4495
4700
  } catch {
4496
4701
  return [];
4497
4702
  }
@@ -4499,7 +4704,7 @@ function readQueue() {
4499
4704
  function writeQueue(queue) {
4500
4705
  ensureDir();
4501
4706
  const tmp = `${QUEUE_PATH}.tmp`;
4502
- writeFileSync3(tmp, JSON.stringify(queue, null, 2));
4707
+ writeFileSync4(tmp, JSON.stringify(queue, null, 2));
4503
4708
  renameSync3(tmp, QUEUE_PATH);
4504
4709
  }
4505
4710
  function queueIntercom(targetSession, reason) {
@@ -4523,25 +4728,25 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
4523
4728
  var init_intercom_queue = __esm({
4524
4729
  "src/lib/intercom-queue.ts"() {
4525
4730
  "use strict";
4526
- QUEUE_PATH = path8.join(os6.homedir(), ".exe-os", "intercom-queue.json");
4731
+ QUEUE_PATH = path9.join(os6.homedir(), ".exe-os", "intercom-queue.json");
4527
4732
  TTL_MS = 60 * 60 * 1e3;
4528
- INTERCOM_LOG = path8.join(os6.homedir(), ".exe-os", "intercom.log");
4733
+ INTERCOM_LOG = path9.join(os6.homedir(), ".exe-os", "intercom.log");
4529
4734
  }
4530
4735
  });
4531
4736
 
4532
4737
  // src/lib/license.ts
4533
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
4738
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
4534
4739
  import { randomUUID as randomUUID11 } from "crypto";
4535
- import path9 from "path";
4740
+ import path10 from "path";
4536
4741
  import { jwtVerify, importSPKI } from "jose";
4537
4742
  var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
4538
4743
  var init_license = __esm({
4539
4744
  "src/lib/license.ts"() {
4540
4745
  "use strict";
4541
4746
  init_config();
4542
- LICENSE_PATH = path9.join(EXE_AI_DIR, "license.key");
4543
- CACHE_PATH = path9.join(EXE_AI_DIR, "license-cache.json");
4544
- DEVICE_ID_PATH = path9.join(EXE_AI_DIR, "device-id");
4747
+ LICENSE_PATH = path10.join(EXE_AI_DIR, "license.key");
4748
+ CACHE_PATH = path10.join(EXE_AI_DIR, "license-cache.json");
4749
+ DEVICE_ID_PATH = path10.join(EXE_AI_DIR, "device-id");
4545
4750
  PLAN_LIMITS = {
4546
4751
  free: { devices: 1, employees: 1, memories: 5e3 },
4547
4752
  pro: { devices: 3, employees: 5, memories: 1e5 },
@@ -4553,12 +4758,12 @@ var init_license = __esm({
4553
4758
  });
4554
4759
 
4555
4760
  // src/lib/plan-limits.ts
4556
- import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
4557
- import path10 from "path";
4761
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
4762
+ import path11 from "path";
4558
4763
  function getLicenseSync() {
4559
4764
  try {
4560
- if (!existsSync9(CACHE_PATH2)) return freeLicense();
4561
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
4765
+ if (!existsSync10(CACHE_PATH2)) return freeLicense();
4766
+ const raw = JSON.parse(readFileSync9(CACHE_PATH2, "utf8"));
4562
4767
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
4563
4768
  const parts = raw.token.split(".");
4564
4769
  if (parts.length !== 3) return freeLicense();
@@ -4596,8 +4801,8 @@ function assertEmployeeLimitSync(rosterPath) {
4596
4801
  const filePath = rosterPath ?? EMPLOYEES_PATH;
4597
4802
  let count = 0;
4598
4803
  try {
4599
- if (existsSync9(filePath)) {
4600
- const raw = readFileSync8(filePath, "utf8");
4804
+ if (existsSync10(filePath)) {
4805
+ const raw = readFileSync9(filePath, "utf8");
4601
4806
  const employees = JSON.parse(raw);
4602
4807
  count = Array.isArray(employees) ? employees.length : 0;
4603
4808
  }
@@ -4626,19 +4831,19 @@ var init_plan_limits = __esm({
4626
4831
  this.name = "PlanLimitError";
4627
4832
  }
4628
4833
  };
4629
- CACHE_PATH2 = path10.join(EXE_AI_DIR, "license-cache.json");
4834
+ CACHE_PATH2 = path11.join(EXE_AI_DIR, "license-cache.json");
4630
4835
  }
4631
4836
  });
4632
4837
 
4633
4838
  // src/lib/notifications.ts
4634
4839
  import crypto3 from "crypto";
4635
- import path11 from "path";
4840
+ import path12 from "path";
4636
4841
  import os7 from "os";
4637
4842
  import {
4638
- readFileSync as readFileSync9,
4843
+ readFileSync as readFileSync10,
4639
4844
  readdirSync as readdirSync2,
4640
4845
  unlinkSync as unlinkSync3,
4641
- existsSync as existsSync10,
4846
+ existsSync as existsSync11,
4642
4847
  rmdirSync
4643
4848
  } from "fs";
4644
4849
  async function writeNotification(notification) {
@@ -4742,10 +4947,11 @@ var init_task_scope = __esm({
4742
4947
 
4743
4948
  // src/lib/tasks-crud.ts
4744
4949
  import crypto5 from "crypto";
4745
- import path12 from "path";
4950
+ import path13 from "path";
4951
+ import os8 from "os";
4746
4952
  import { execSync as execSync4 } from "child_process";
4747
4953
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
4748
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
4954
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
4749
4955
  async function writeCheckpoint(input) {
4750
4956
  const client = getClient();
4751
4957
  const row = await resolveTask(client, input.taskId);
@@ -4786,6 +4992,35 @@ function extractParentFromContext(contextBody) {
4786
4992
  function slugify(title) {
4787
4993
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
4788
4994
  }
4995
+ function buildKeywordIndex() {
4996
+ const idx = /* @__PURE__ */ new Map();
4997
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
4998
+ for (const kw of keywords) {
4999
+ const existing = idx.get(kw) ?? [];
5000
+ existing.push(role);
5001
+ idx.set(kw, existing);
5002
+ }
5003
+ }
5004
+ return idx;
5005
+ }
5006
+ function checkLaneAffinity(title, context, assigneeName) {
5007
+ const employees = loadEmployeesSync();
5008
+ const employee = employees.find((e) => e.name === assigneeName);
5009
+ if (!employee) return void 0;
5010
+ const assigneeRole = employee.role;
5011
+ const text = `${title} ${context}`.toLowerCase();
5012
+ const matchedRoles = /* @__PURE__ */ new Set();
5013
+ for (const [keyword, roles] of KEYWORD_INDEX) {
5014
+ if (text.includes(keyword)) {
5015
+ for (const role of roles) matchedRoles.add(role);
5016
+ }
5017
+ }
5018
+ if (matchedRoles.size === 0) return void 0;
5019
+ if (matchedRoles.has(assigneeRole)) return void 0;
5020
+ if (assigneeRole === "COO") return void 0;
5021
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
5022
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
5023
+ }
4789
5024
  async function resolveTask(client, identifier, scopeSession) {
4790
5025
  const scope = sessionScopeFilter(scopeSession);
4791
5026
  let result = await client.execute({
@@ -4835,7 +5070,14 @@ async function createTaskCore(input) {
4835
5070
  const id = crypto5.randomUUID();
4836
5071
  const now = (/* @__PURE__ */ new Date()).toISOString();
4837
5072
  const slug = slugify(input.title);
4838
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
5073
+ let earlySessionScope = null;
5074
+ try {
5075
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
5076
+ earlySessionScope = resolveExeSession2();
5077
+ } catch {
5078
+ }
5079
+ const scope = earlySessionScope ?? "default";
5080
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
4839
5081
  let blockedById = null;
4840
5082
  const initialStatus = input.blockedBy ? "blocked" : "open";
4841
5083
  if (input.blockedBy) {
@@ -4875,22 +5117,24 @@ async function createTaskCore(input) {
4875
5117
  if (dupCheck.rows.length > 0) {
4876
5118
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
4877
5119
  }
5120
+ if (!process.env.DISABLE_LANE_AFFINITY) {
5121
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
5122
+ if (laneWarning) {
5123
+ warning = warning ? `${warning}
5124
+ ${laneWarning}` : laneWarning;
5125
+ }
5126
+ }
4878
5127
  if (input.baseDir) {
4879
5128
  try {
4880
- await mkdir4(path12.join(input.baseDir, "exe", "output"), { recursive: true });
4881
- await mkdir4(path12.join(input.baseDir, "exe", "research"), { recursive: true });
5129
+ await mkdir4(path13.join(input.baseDir, "exe", "output"), { recursive: true });
5130
+ await mkdir4(path13.join(input.baseDir, "exe", "research"), { recursive: true });
4882
5131
  await ensureArchitectureDoc(input.baseDir, input.projectName);
4883
5132
  await ensureGitignoreExe(input.baseDir);
4884
5133
  } catch {
4885
5134
  }
4886
5135
  }
4887
5136
  const complexity = input.complexity ?? "standard";
4888
- let sessionScope = null;
4889
- try {
4890
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
4891
- sessionScope = resolveExeSession2();
4892
- } catch {
4893
- }
5137
+ const sessionScope = earlySessionScope;
4894
5138
  await client.execute({
4895
5139
  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)
4896
5140
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -4917,6 +5161,43 @@ async function createTaskCore(input) {
4917
5161
  now
4918
5162
  ]
4919
5163
  });
5164
+ if (input.baseDir) {
5165
+ try {
5166
+ const EXE_OS_DIR = path13.join(os8.homedir(), ".exe-os");
5167
+ const mdPath = path13.join(EXE_OS_DIR, taskFile);
5168
+ const mdDir = path13.dirname(mdPath);
5169
+ if (!existsSync12(mdDir)) await mkdir4(mdDir, { recursive: true });
5170
+ const reviewer = input.reviewer ?? input.assignedBy;
5171
+ const mdContent = `# ${input.title}
5172
+
5173
+ **ID:** ${id}
5174
+ **Status:** ${initialStatus}
5175
+ **Priority:** ${input.priority}
5176
+ **Assigned by:** ${input.assignedBy}
5177
+ **Assigned to:** ${input.assignedTo}
5178
+ **Project:** ${input.projectName}
5179
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
5180
+ **Parent task:** ${parentTaskId}` : ""}
5181
+ **Reviewer:** ${reviewer}
5182
+
5183
+ ## Context
5184
+
5185
+ ${input.context}
5186
+
5187
+ ## MANDATORY: When done
5188
+
5189
+ You MUST call update_task with status "done" and a result summary when finished.
5190
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
5191
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
5192
+ `;
5193
+ await writeFile4(mdPath, mdContent, "utf-8");
5194
+ } catch (err) {
5195
+ process.stderr.write(
5196
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
5197
+ `
5198
+ );
5199
+ }
5200
+ }
4920
5201
  return {
4921
5202
  id,
4922
5203
  title: input.title,
@@ -5109,7 +5390,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
5109
5390
  return { row, taskFile, now, taskId };
5110
5391
  }
5111
5392
  }
5112
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
5393
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
5113
5394
  process.stderr.write(
5114
5395
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
5115
5396
  `
@@ -5174,9 +5455,9 @@ async function deleteTaskCore(taskId, _baseDir) {
5174
5455
  return { taskFile, assignedTo, assignedBy, taskSlug };
5175
5456
  }
5176
5457
  async function ensureArchitectureDoc(baseDir, projectName) {
5177
- const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
5458
+ const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
5178
5459
  try {
5179
- if (existsSync11(archPath)) return;
5460
+ if (existsSync12(archPath)) return;
5180
5461
  const template = [
5181
5462
  `# ${projectName} \u2014 System Architecture`,
5182
5463
  "",
@@ -5209,10 +5490,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
5209
5490
  }
5210
5491
  }
5211
5492
  async function ensureGitignoreExe(baseDir) {
5212
- const gitignorePath = path12.join(baseDir, ".gitignore");
5493
+ const gitignorePath = path13.join(baseDir, ".gitignore");
5213
5494
  try {
5214
- if (existsSync11(gitignorePath)) {
5215
- const content = readFileSync10(gitignorePath, "utf-8");
5495
+ if (existsSync12(gitignorePath)) {
5496
+ const content = readFileSync11(gitignorePath, "utf-8");
5216
5497
  if (/^\/?exe\/?$/m.test(content)) return;
5217
5498
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
5218
5499
  } else {
@@ -5221,20 +5502,30 @@ async function ensureGitignoreExe(baseDir) {
5221
5502
  } catch {
5222
5503
  }
5223
5504
  }
5224
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
5505
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
5225
5506
  var init_tasks_crud = __esm({
5226
5507
  "src/lib/tasks-crud.ts"() {
5227
5508
  "use strict";
5228
5509
  init_database();
5229
5510
  init_task_scope();
5511
+ init_employees();
5512
+ LANE_KEYWORDS = {
5513
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
5514
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
5515
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
5516
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
5517
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
5518
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
5519
+ };
5520
+ KEYWORD_INDEX = buildKeywordIndex();
5230
5521
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
5231
5522
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
5232
5523
  }
5233
5524
  });
5234
5525
 
5235
5526
  // src/lib/tasks-review.ts
5236
- import path13 from "path";
5237
- import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
5527
+ import path14 from "path";
5528
+ import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
5238
5529
  async function countPendingReviews(sessionScope) {
5239
5530
  const client = getClient();
5240
5531
  if (sessionScope) {
@@ -5256,7 +5547,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
5256
5547
  const result2 = await client.execute({
5257
5548
  sql: `SELECT COUNT(*) as cnt FROM tasks
5258
5549
  WHERE status = 'needs_review' AND updated_at > ?
5259
- AND (session_scope = ? OR session_scope IS NULL)`,
5550
+ AND session_scope = ?`,
5260
5551
  args: [sinceIso, sessionScope]
5261
5552
  });
5262
5553
  return Number(result2.rows[0]?.cnt) || 0;
@@ -5274,7 +5565,7 @@ async function listPendingReviews(limit, sessionScope) {
5274
5565
  const result2 = await client.execute({
5275
5566
  sql: `SELECT title, assigned_to, project_name FROM tasks
5276
5567
  WHERE status = 'needs_review'
5277
- AND (session_scope = ? OR session_scope IS NULL)
5568
+ AND session_scope = ?
5278
5569
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
5279
5570
  args: [sessionScope, limit]
5280
5571
  });
@@ -5395,14 +5686,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
5395
5686
  if (parts.length >= 3 && parts[0] === "review") {
5396
5687
  const agent = parts[1];
5397
5688
  const slug = parts.slice(2).join("-");
5398
- const originalTaskFile = `exe/${agent}/${slug}.md`;
5689
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
5399
5690
  const result = await client.execute({
5400
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
5401
- args: [now, originalTaskFile]
5691
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
5692
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
5402
5693
  });
5403
5694
  if (result.rowsAffected > 0) {
5404
5695
  process.stderr.write(
5405
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
5696
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
5406
5697
  `
5407
5698
  );
5408
5699
  }
@@ -5415,11 +5706,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
5415
5706
  );
5416
5707
  }
5417
5708
  try {
5418
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
5419
- if (existsSync12(cacheDir)) {
5709
+ const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
5710
+ if (existsSync13(cacheDir)) {
5420
5711
  for (const f of readdirSync3(cacheDir)) {
5421
5712
  if (f.startsWith("review-notified-")) {
5422
- unlinkSync4(path13.join(cacheDir, f));
5713
+ unlinkSync4(path14.join(cacheDir, f));
5423
5714
  }
5424
5715
  }
5425
5716
  }
@@ -5440,7 +5731,7 @@ var init_tasks_review = __esm({
5440
5731
  });
5441
5732
 
5442
5733
  // src/lib/tasks-chain.ts
5443
- import path14 from "path";
5734
+ import path15 from "path";
5444
5735
  import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
5445
5736
  async function cascadeUnblock(taskId, baseDir, now) {
5446
5737
  const client = getClient();
@@ -5457,7 +5748,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
5457
5748
  });
5458
5749
  for (const ur of unblockedRows.rows) {
5459
5750
  try {
5460
- const ubFile = path14.join(baseDir, String(ur.task_file));
5751
+ const ubFile = path15.join(baseDir, String(ur.task_file));
5461
5752
  let ubContent = await readFile4(ubFile, "utf-8");
5462
5753
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
5463
5754
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -5526,7 +5817,7 @@ var init_tasks_chain = __esm({
5526
5817
 
5527
5818
  // src/lib/project-name.ts
5528
5819
  import { execSync as execSync5 } from "child_process";
5529
- import path15 from "path";
5820
+ import path16 from "path";
5530
5821
  function getProjectName(cwd) {
5531
5822
  const dir = cwd ?? process.cwd();
5532
5823
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -5539,7 +5830,7 @@ function getProjectName(cwd) {
5539
5830
  timeout: 2e3,
5540
5831
  stdio: ["pipe", "pipe", "pipe"]
5541
5832
  }).trim();
5542
- repoRoot = path15.dirname(gitCommonDir);
5833
+ repoRoot = path16.dirname(gitCommonDir);
5543
5834
  } catch {
5544
5835
  repoRoot = execSync5("git rev-parse --show-toplevel", {
5545
5836
  cwd: dir,
@@ -5548,11 +5839,11 @@ function getProjectName(cwd) {
5548
5839
  stdio: ["pipe", "pipe", "pipe"]
5549
5840
  }).trim();
5550
5841
  }
5551
- _cached2 = path15.basename(repoRoot);
5842
+ _cached2 = path16.basename(repoRoot);
5552
5843
  _cachedCwd = dir;
5553
5844
  return _cached2;
5554
5845
  } catch {
5555
- _cached2 = path15.basename(dir);
5846
+ _cached2 = path16.basename(dir);
5556
5847
  _cachedCwd = dir;
5557
5848
  return _cached2;
5558
5849
  }
@@ -5584,7 +5875,7 @@ function findSessionForProject(projectName) {
5584
5875
  const sessions = listSessions();
5585
5876
  for (const s of sessions) {
5586
5877
  const proj = s.projectDir.split("/").filter(Boolean).pop();
5587
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
5878
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
5588
5879
  }
5589
5880
  return null;
5590
5881
  }
@@ -5630,7 +5921,7 @@ var init_session_scope = __esm({
5630
5921
 
5631
5922
  // src/lib/tasks-notify.ts
5632
5923
  async function dispatchTaskToEmployee(input) {
5633
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
5924
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
5634
5925
  let crossProject = false;
5635
5926
  if (input.projectName) {
5636
5927
  try {
@@ -6025,8 +6316,8 @@ __export(tasks_exports, {
6025
6316
  updateTaskStatus: () => updateTaskStatus,
6026
6317
  writeCheckpoint: () => writeCheckpoint
6027
6318
  });
6028
- import path16 from "path";
6029
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
6319
+ import path17 from "path";
6320
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
6030
6321
  async function createTask(input) {
6031
6322
  const result = await createTaskCore(input);
6032
6323
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -6045,11 +6336,11 @@ async function updateTask(input) {
6045
6336
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
6046
6337
  try {
6047
6338
  const agent = String(row.assigned_to);
6048
- const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
6049
- const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
6339
+ const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
6340
+ const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
6050
6341
  if (input.status === "in_progress") {
6051
- mkdirSync6(cacheDir, { recursive: true });
6052
- writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
6342
+ mkdirSync7(cacheDir, { recursive: true });
6343
+ writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
6053
6344
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
6054
6345
  try {
6055
6346
  unlinkSync5(cachePath);
@@ -6109,7 +6400,7 @@ async function updateTask(input) {
6109
6400
  }
6110
6401
  const isTerminal = input.status === "done" || input.status === "needs_review";
6111
6402
  if (isTerminal) {
6112
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
6403
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
6113
6404
  if (!isCoordinator) {
6114
6405
  notifyTaskDone();
6115
6406
  }
@@ -6134,7 +6425,7 @@ async function updateTask(input) {
6134
6425
  }
6135
6426
  }
6136
6427
  }
6137
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
6428
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
6138
6429
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
6139
6430
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
6140
6431
  taskId,
@@ -6150,7 +6441,7 @@ async function updateTask(input) {
6150
6441
  });
6151
6442
  }
6152
6443
  let nextTask;
6153
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
6444
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
6154
6445
  try {
6155
6446
  nextTask = await findNextTask(String(row.assigned_to));
6156
6447
  } catch {
@@ -6516,13 +6807,13 @@ __export(tmux_routing_exports, {
6516
6807
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
6517
6808
  });
6518
6809
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
6519
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, existsSync as existsSync13, appendFileSync } from "fs";
6520
- import path17 from "path";
6521
- import os8 from "os";
6810
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8, existsSync as existsSync14, appendFileSync } from "fs";
6811
+ import path18 from "path";
6812
+ import os9 from "os";
6522
6813
  import { fileURLToPath as fileURLToPath2 } from "url";
6523
6814
  import { unlinkSync as unlinkSync6 } from "fs";
6524
6815
  function spawnLockPath(sessionName) {
6525
- return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
6816
+ return path18.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
6526
6817
  }
6527
6818
  function isProcessAlive(pid) {
6528
6819
  try {
@@ -6533,13 +6824,13 @@ function isProcessAlive(pid) {
6533
6824
  }
6534
6825
  }
6535
6826
  function acquireSpawnLock2(sessionName) {
6536
- if (!existsSync13(SPAWN_LOCK_DIR)) {
6537
- mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
6827
+ if (!existsSync14(SPAWN_LOCK_DIR)) {
6828
+ mkdirSync8(SPAWN_LOCK_DIR, { recursive: true });
6538
6829
  }
6539
6830
  const lockFile = spawnLockPath(sessionName);
6540
- if (existsSync13(lockFile)) {
6831
+ if (existsSync14(lockFile)) {
6541
6832
  try {
6542
- const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
6833
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
6543
6834
  const age = Date.now() - lock.timestamp;
6544
6835
  if (isProcessAlive(lock.pid) && age < 6e4) {
6545
6836
  return false;
@@ -6547,7 +6838,7 @@ function acquireSpawnLock2(sessionName) {
6547
6838
  } catch {
6548
6839
  }
6549
6840
  }
6550
- writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
6841
+ writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
6551
6842
  return true;
6552
6843
  }
6553
6844
  function releaseSpawnLock2(sessionName) {
@@ -6559,13 +6850,13 @@ function releaseSpawnLock2(sessionName) {
6559
6850
  function resolveBehaviorsExporterScript() {
6560
6851
  try {
6561
6852
  const thisFile = fileURLToPath2(import.meta.url);
6562
- const scriptPath = path17.join(
6563
- path17.dirname(thisFile),
6853
+ const scriptPath = path18.join(
6854
+ path18.dirname(thisFile),
6564
6855
  "..",
6565
6856
  "bin",
6566
6857
  "exe-export-behaviors.js"
6567
6858
  );
6568
- return existsSync13(scriptPath) ? scriptPath : null;
6859
+ return existsSync14(scriptPath) ? scriptPath : null;
6569
6860
  } catch {
6570
6861
  return null;
6571
6862
  }
@@ -6631,12 +6922,12 @@ function extractRootExe(name) {
6631
6922
  return parts.length > 0 ? parts[parts.length - 1] : null;
6632
6923
  }
6633
6924
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
6634
- if (!existsSync13(SESSION_CACHE)) {
6635
- mkdirSync7(SESSION_CACHE, { recursive: true });
6925
+ if (!existsSync14(SESSION_CACHE)) {
6926
+ mkdirSync8(SESSION_CACHE, { recursive: true });
6636
6927
  }
6637
6928
  const rootExe = extractRootExe(parentExe) ?? parentExe;
6638
- const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
6639
- writeFileSync6(filePath, JSON.stringify({
6929
+ const filePath = path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
6930
+ writeFileSync7(filePath, JSON.stringify({
6640
6931
  parentExe: rootExe,
6641
6932
  dispatchedBy: dispatchedBy || rootExe,
6642
6933
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -6644,7 +6935,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
6644
6935
  }
6645
6936
  function getParentExe(sessionKey) {
6646
6937
  try {
6647
- const data = JSON.parse(readFileSync11(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
6938
+ const data = JSON.parse(readFileSync12(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
6648
6939
  return data.parentExe || null;
6649
6940
  } catch {
6650
6941
  return null;
@@ -6652,8 +6943,8 @@ function getParentExe(sessionKey) {
6652
6943
  }
6653
6944
  function getDispatchedBy(sessionKey) {
6654
6945
  try {
6655
- const data = JSON.parse(readFileSync11(
6656
- path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
6946
+ const data = JSON.parse(readFileSync12(
6947
+ path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
6657
6948
  "utf8"
6658
6949
  ));
6659
6950
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -6714,32 +7005,50 @@ async function verifyPaneAtCapacity(sessionName) {
6714
7005
  }
6715
7006
  function readDebounceState() {
6716
7007
  try {
6717
- if (!existsSync13(DEBOUNCE_FILE)) return {};
6718
- return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
7008
+ if (!existsSync14(DEBOUNCE_FILE)) return {};
7009
+ const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
7010
+ const state = {};
7011
+ for (const [key, val] of Object.entries(raw)) {
7012
+ if (typeof val === "number") {
7013
+ state[key] = { lastSent: val, pending: 0 };
7014
+ } else if (val && typeof val === "object" && "lastSent" in val) {
7015
+ state[key] = val;
7016
+ }
7017
+ }
7018
+ return state;
6719
7019
  } catch {
6720
7020
  return {};
6721
7021
  }
6722
7022
  }
6723
7023
  function writeDebounceState(state) {
6724
7024
  try {
6725
- if (!existsSync13(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
6726
- writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
7025
+ if (!existsSync14(SESSION_CACHE)) mkdirSync8(SESSION_CACHE, { recursive: true });
7026
+ writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
6727
7027
  } catch {
6728
7028
  }
6729
7029
  }
6730
7030
  function isDebounced(targetSession) {
6731
7031
  const state = readDebounceState();
6732
- const lastSent = state[targetSession] ?? 0;
6733
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
7032
+ const entry = state[targetSession];
7033
+ const lastSent = entry?.lastSent ?? 0;
7034
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
7035
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
7036
+ state[targetSession].pending++;
7037
+ writeDebounceState(state);
7038
+ return true;
7039
+ }
7040
+ return false;
6734
7041
  }
6735
7042
  function recordDebounce(targetSession) {
6736
7043
  const state = readDebounceState();
6737
- state[targetSession] = Date.now();
7044
+ const batched = state[targetSession]?.pending ?? 0;
7045
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
6738
7046
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
6739
7047
  for (const key of Object.keys(state)) {
6740
- if ((state[key] ?? 0) < cutoff) delete state[key];
7048
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
6741
7049
  }
6742
7050
  writeDebounceState(state);
7051
+ return batched;
6743
7052
  }
6744
7053
  function logIntercom(msg) {
6745
7054
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -6784,7 +7093,7 @@ function sendIntercom(targetSession) {
6784
7093
  return "skipped_exe";
6785
7094
  }
6786
7095
  if (isDebounced(targetSession)) {
6787
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
7096
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
6788
7097
  return "debounced";
6789
7098
  }
6790
7099
  try {
@@ -6796,14 +7105,14 @@ function sendIntercom(targetSession) {
6796
7105
  const sessionState = getSessionState(targetSession);
6797
7106
  if (sessionState === "no_claude") {
6798
7107
  queueIntercom(targetSession, "claude not running in session");
6799
- recordDebounce(targetSession);
6800
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
7108
+ const batched2 = recordDebounce(targetSession);
7109
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
6801
7110
  return "queued";
6802
7111
  }
6803
7112
  if (sessionState === "thinking" || sessionState === "tool") {
6804
7113
  queueIntercom(targetSession, "session busy at send time");
6805
- recordDebounce(targetSession);
6806
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
7114
+ const batched2 = recordDebounce(targetSession);
7115
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
6807
7116
  return "queued";
6808
7117
  }
6809
7118
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -6811,8 +7120,8 @@ function sendIntercom(targetSession) {
6811
7120
  transport.sendKeys(targetSession, "q");
6812
7121
  }
6813
7122
  transport.sendKeys(targetSession, "/exe-intercom");
6814
- recordDebounce(targetSession);
6815
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
7123
+ const batched = recordDebounce(targetSession);
7124
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
6816
7125
  return "delivered";
6817
7126
  } catch {
6818
7127
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -6842,7 +7151,7 @@ function notifyParentExe(sessionKey) {
6842
7151
  return true;
6843
7152
  }
6844
7153
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
6845
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
7154
+ if (isCoordinatorName(employeeName)) {
6846
7155
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
6847
7156
  }
6848
7157
  try {
@@ -6914,26 +7223,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6914
7223
  const transport = getTransport();
6915
7224
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
6916
7225
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
6917
- const logDir = path17.join(os8.homedir(), ".exe-os", "session-logs");
6918
- const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
6919
- if (!existsSync13(logDir)) {
6920
- mkdirSync7(logDir, { recursive: true });
7226
+ const logDir = path18.join(os9.homedir(), ".exe-os", "session-logs");
7227
+ const logFile = path18.join(logDir, `${instanceLabel}-${Date.now()}.log`);
7228
+ if (!existsSync14(logDir)) {
7229
+ mkdirSync8(logDir, { recursive: true });
6921
7230
  }
6922
7231
  transport.kill(sessionName);
6923
7232
  let cleanupSuffix = "";
6924
7233
  try {
6925
7234
  const thisFile = fileURLToPath2(import.meta.url);
6926
- const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
6927
- if (existsSync13(cleanupScript)) {
7235
+ const cleanupScript = path18.join(path18.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
7236
+ if (existsSync14(cleanupScript)) {
6928
7237
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
6929
7238
  }
6930
7239
  } catch {
6931
7240
  }
6932
7241
  try {
6933
- const claudeJsonPath = path17.join(os8.homedir(), ".claude.json");
7242
+ const claudeJsonPath = path18.join(os9.homedir(), ".claude.json");
6934
7243
  let claudeJson = {};
6935
7244
  try {
6936
- claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
7245
+ claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
6937
7246
  } catch {
6938
7247
  }
6939
7248
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -6941,17 +7250,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6941
7250
  const trustDir = opts?.cwd ?? projectDir;
6942
7251
  if (!projects[trustDir]) projects[trustDir] = {};
6943
7252
  projects[trustDir].hasTrustDialogAccepted = true;
6944
- writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
7253
+ writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
6945
7254
  } catch {
6946
7255
  }
6947
7256
  try {
6948
- const settingsDir = path17.join(os8.homedir(), ".claude", "projects");
7257
+ const settingsDir = path18.join(os9.homedir(), ".claude", "projects");
6949
7258
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
6950
- const projSettingsDir = path17.join(settingsDir, normalizedKey);
6951
- const settingsPath = path17.join(projSettingsDir, "settings.json");
7259
+ const projSettingsDir = path18.join(settingsDir, normalizedKey);
7260
+ const settingsPath = path18.join(projSettingsDir, "settings.json");
6952
7261
  let settings = {};
6953
7262
  try {
6954
- settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
7263
+ settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
6955
7264
  } catch {
6956
7265
  }
6957
7266
  const perms = settings.permissions ?? {};
@@ -6979,21 +7288,24 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6979
7288
  if (changed) {
6980
7289
  perms.allow = allow;
6981
7290
  settings.permissions = perms;
6982
- mkdirSync7(projSettingsDir, { recursive: true });
6983
- writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
7291
+ mkdirSync8(projSettingsDir, { recursive: true });
7292
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
6984
7293
  }
6985
7294
  } catch {
6986
7295
  }
6987
7296
  const spawnCwd = opts?.cwd ?? projectDir;
6988
7297
  const useExeAgent = !!(opts?.model && opts?.provider);
6989
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
7298
+ const agentRtConfig = getAgentRuntime(employeeName);
7299
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
7300
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
7301
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
6990
7302
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
6991
7303
  let identityFlag = "";
6992
7304
  let behaviorsFlag = "";
6993
7305
  let legacyFallbackWarned = false;
6994
7306
  if (!useExeAgent && !useBinSymlink) {
6995
- const identityPath = path17.join(
6996
- os8.homedir(),
7307
+ const identityPath = path18.join(
7308
+ os9.homedir(),
6997
7309
  ".exe-os",
6998
7310
  "identity",
6999
7311
  `${employeeName}.md`
@@ -7002,13 +7314,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7002
7314
  const hasAgentFlag = claudeSupportsAgentFlag();
7003
7315
  if (hasAgentFlag) {
7004
7316
  identityFlag = ` --agent ${employeeName}`;
7005
- } else if (existsSync13(identityPath)) {
7317
+ } else if (existsSync14(identityPath)) {
7006
7318
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
7007
7319
  legacyFallbackWarned = true;
7008
7320
  }
7009
7321
  const behaviorsFile = exportBehaviorsSync(
7010
7322
  employeeName,
7011
- path17.basename(spawnCwd),
7323
+ path18.basename(spawnCwd),
7012
7324
  sessionName
7013
7325
  );
7014
7326
  if (behaviorsFile) {
@@ -7023,16 +7335,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7023
7335
  }
7024
7336
  let sessionContextFlag = "";
7025
7337
  try {
7026
- const ctxDir = path17.join(os8.homedir(), ".exe-os", "session-cache");
7027
- mkdirSync7(ctxDir, { recursive: true });
7028
- const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
7338
+ const ctxDir = path18.join(os9.homedir(), ".exe-os", "session-cache");
7339
+ mkdirSync8(ctxDir, { recursive: true });
7340
+ const ctxFile = path18.join(ctxDir, `session-context-${sessionName}.md`);
7029
7341
  const ctxContent = [
7030
7342
  `## Session Context`,
7031
7343
  `You are running in tmux session: ${sessionName}.`,
7032
7344
  `Your parent coordinator session is ${exeSession}.`,
7033
7345
  `Your employees (if any) use the -${exeSession} suffix.`
7034
7346
  ].join("\n");
7035
- writeFileSync6(ctxFile, ctxContent);
7347
+ writeFileSync7(ctxFile, ctxContent);
7036
7348
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
7037
7349
  } catch {
7038
7350
  }
@@ -7046,9 +7358,48 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7046
7358
  }
7047
7359
  }
7048
7360
  }
7361
+ if (useCodex) {
7362
+ const codexCfg = RUNTIME_TABLE.codex;
7363
+ if (codexCfg?.apiKeyEnv) {
7364
+ const keyVal = process.env[codexCfg.apiKeyEnv];
7365
+ if (keyVal) {
7366
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
7367
+ }
7368
+ }
7369
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
7370
+ }
7371
+ if (useOpencode) {
7372
+ const ocCfg = PROVIDER_TABLE.opencode;
7373
+ if (ocCfg?.apiKeyEnv) {
7374
+ const keyVal = process.env[ocCfg.apiKeyEnv];
7375
+ if (keyVal) {
7376
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
7377
+ }
7378
+ }
7379
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
7380
+ }
7381
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
7382
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
7383
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
7384
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
7385
+ }
7386
+ }
7049
7387
  let spawnCommand;
7050
7388
  if (useExeAgent) {
7051
7389
  spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
7390
+ } else if (useCodex) {
7391
+ process.stderr.write(
7392
+ `[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
7393
+ `
7394
+ );
7395
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName}${cleanupSuffix}`;
7396
+ } else if (useOpencode) {
7397
+ const binName = `${employeeName}-opencode`;
7398
+ process.stderr.write(
7399
+ `[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
7400
+ `
7401
+ );
7402
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
7052
7403
  } else if (useBinSymlink) {
7053
7404
  const binName = `${employeeName}-${ccProvider}`;
7054
7405
  process.stderr.write(
@@ -7070,11 +7421,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7070
7421
  transport.pipeLog(sessionName, logFile);
7071
7422
  try {
7072
7423
  const mySession = getMySession();
7073
- const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
7074
- writeFileSync6(dispatchInfo, JSON.stringify({
7424
+ const dispatchInfo = path18.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
7425
+ writeFileSync7(dispatchInfo, JSON.stringify({
7075
7426
  dispatchedBy: mySession,
7076
7427
  rootExe: exeSession,
7077
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
7428
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
7429
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
7430
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
7078
7431
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
7079
7432
  }));
7080
7433
  } catch {
@@ -7092,6 +7445,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7092
7445
  booted = true;
7093
7446
  break;
7094
7447
  }
7448
+ } else if (useCodex) {
7449
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
7450
+ booted = true;
7451
+ break;
7452
+ }
7095
7453
  } else {
7096
7454
  if (pane.includes("Claude Code") || pane.includes("\u276F")) {
7097
7455
  booted = true;
@@ -7103,9 +7461,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
7103
7461
  }
7104
7462
  if (!booted) {
7105
7463
  releaseSpawnLock2(sessionName);
7106
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
7464
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
7465
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
7107
7466
  }
7108
- if (!useExeAgent) {
7467
+ if (!useExeAgent && !useCodex) {
7109
7468
  try {
7110
7469
  transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
7111
7470
  } catch {
@@ -7132,17 +7491,19 @@ var init_tmux_routing = __esm({
7132
7491
  init_cc_agent_support();
7133
7492
  init_mcp_prefix();
7134
7493
  init_provider_table();
7494
+ init_agent_config();
7495
+ init_runtime_table();
7135
7496
  init_intercom_queue();
7136
7497
  init_plan_limits();
7137
7498
  init_employees();
7138
- SPAWN_LOCK_DIR = path17.join(os8.homedir(), ".exe-os", "spawn-locks");
7139
- SESSION_CACHE = path17.join(os8.homedir(), ".exe-os", "session-cache");
7499
+ SPAWN_LOCK_DIR = path18.join(os9.homedir(), ".exe-os", "spawn-locks");
7500
+ SESSION_CACHE = path18.join(os9.homedir(), ".exe-os", "session-cache");
7140
7501
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
7141
7502
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
7142
7503
  VERIFY_PANE_LINES = 200;
7143
7504
  INTERCOM_DEBOUNCE_MS = 3e4;
7144
- INTERCOM_LOG2 = path17.join(os8.homedir(), ".exe-os", "intercom.log");
7145
- DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
7505
+ INTERCOM_LOG2 = path18.join(os9.homedir(), ".exe-os", "intercom.log");
7506
+ DEBOUNCE_FILE = path18.join(SESSION_CACHE, "intercom-debounce.json");
7146
7507
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
7147
7508
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
7148
7509
  }
@@ -8051,10 +8412,49 @@ function buildPermissionContext(platform, permissions) {
8051
8412
  return `[${platform.toUpperCase()} \u2014 allowed: ${parts.join(", ")}]`;
8052
8413
  }
8053
8414
 
8415
+ // src/gateway/bot-errors.ts
8416
+ var FatalBotError = class extends Error {
8417
+ constructor(message, cause) {
8418
+ super(message);
8419
+ this.cause = cause;
8420
+ this.name = "FatalBotError";
8421
+ }
8422
+ fatal = true;
8423
+ };
8424
+ var RecoverableBotError = class extends Error {
8425
+ constructor(message, toolName, cause) {
8426
+ super(message);
8427
+ this.toolName = toolName;
8428
+ this.cause = cause;
8429
+ this.name = "RecoverableBotError";
8430
+ }
8431
+ recoverable = true;
8432
+ };
8433
+ var MaxStepsError = class extends Error {
8434
+ constructor(stepsTaken, maxSteps) {
8435
+ super(
8436
+ `Reached maximum steps (${stepsTaken}/${maxSteps}). Returning partial result.`
8437
+ );
8438
+ this.stepsTaken = stepsTaken;
8439
+ this.maxSteps = maxSteps;
8440
+ this.name = "MaxStepsError";
8441
+ }
8442
+ };
8443
+ function classifyError(err, toolName) {
8444
+ if (err instanceof FatalBotError) return err;
8445
+ if (err instanceof RecoverableBotError) return err;
8446
+ const message = err instanceof Error ? err.message : String(err);
8447
+ if (message.includes("401") || message.includes("403") || message.includes("authentication") || message.includes("rate_limit")) {
8448
+ return new FatalBotError(message, err);
8449
+ }
8450
+ return new RecoverableBotError(message, toolName, err);
8451
+ }
8452
+
8054
8453
  // src/gateway/bot-runtime.ts
8055
8454
  var DEFAULT_MODEL = "claude-sonnet-4-20250514";
8056
8455
  var MAX_TURNS = 10;
8057
8456
  var MAX_HISTORY = 50;
8457
+ var DEFAULT_PLANNING_INTERVAL = 3;
8058
8458
  function buildExecAssistantSystemPrompt(platform, permissions) {
8059
8459
  const permContext = buildPermissionContext(platform, permissions);
8060
8460
  return `You are the founder's executive assistant (agent_id: "ea").
@@ -8106,7 +8506,7 @@ var BotRuntime = class {
8106
8506
  async processMessage(msg, permissions) {
8107
8507
  const sessionKey = msg.chatType === "group" ? msg.channelId : msg.senderId;
8108
8508
  const history = this.getHistory(sessionKey);
8109
- history.push({ role: "user", content: msg.text });
8509
+ history.push({ role: "user", content: msg.text, frameType: "task" });
8110
8510
  const systemPrompt = this.config.systemPrompt + "\n\n" + buildPermissionContext(msg.platform, permissions);
8111
8511
  const allowedTools = filterToolsForPermissions(
8112
8512
  this.config.tools,
@@ -8114,29 +8514,54 @@ var BotRuntime = class {
8114
8514
  );
8115
8515
  const model = this.config.model ?? DEFAULT_MODEL;
8116
8516
  const maxTurns = this.config.maxTurns ?? MAX_TURNS;
8517
+ const planningInterval = this.config.planningInterval ?? DEFAULT_PLANNING_INTERVAL;
8117
8518
  let turns = 0;
8118
8519
  while (turns < maxTurns) {
8119
8520
  turns++;
8120
- const response = await this.client.messages.create({
8121
- model,
8122
- max_tokens: 4096,
8123
- system: systemPrompt,
8124
- messages: history.map((m) => ({
8125
- role: m.role,
8126
- content: m.content
8127
- })),
8128
- tools: allowedTools.map((t) => ({
8129
- name: t.name,
8130
- description: t.description,
8131
- input_schema: t.input_schema
8132
- }))
8133
- });
8521
+ if (planningInterval > 0 && turns > 1 && turns % planningInterval === 1 && maxTurns - turns >= 2) {
8522
+ history.push({
8523
+ role: "user",
8524
+ content: "[Planning checkpoint] Review what you know so far. What facts have you gathered? What is your plan for the remaining steps? Be concise.",
8525
+ frameType: "planning"
8526
+ });
8527
+ }
8528
+ let response;
8529
+ try {
8530
+ response = await this.client.messages.create({
8531
+ model,
8532
+ max_tokens: 4096,
8533
+ system: systemPrompt,
8534
+ messages: history.map((m) => ({
8535
+ role: m.role,
8536
+ content: m.content
8537
+ })),
8538
+ tools: allowedTools.map((t) => ({
8539
+ name: t.name,
8540
+ description: t.description,
8541
+ input_schema: t.input_schema
8542
+ }))
8543
+ });
8544
+ } catch (err) {
8545
+ const classified = classifyError(err);
8546
+ if (classified instanceof FatalBotError) {
8547
+ const errorMsg = `Bot error: ${classified.message}`;
8548
+ history.push({ role: "assistant", content: errorMsg, frameType: "error" });
8549
+ this.trimHistory(sessionKey);
8550
+ return errorMsg;
8551
+ }
8552
+ history.push({
8553
+ role: "assistant",
8554
+ content: `[API error \u2014 retrying] ${classified.message}`,
8555
+ frameType: "error"
8556
+ });
8557
+ continue;
8558
+ }
8134
8559
  const toolUseBlocks = response.content.filter(
8135
8560
  (b) => b.type === "tool_use"
8136
8561
  );
8137
8562
  if (toolUseBlocks.length === 0) {
8138
8563
  const textContent = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
8139
- history.push({ role: "assistant", content: textContent });
8564
+ history.push({ role: "assistant", content: textContent, frameType: "assistant" });
8140
8565
  this.trimHistory(sessionKey);
8141
8566
  return textContent;
8142
8567
  }
@@ -8146,9 +8571,11 @@ var BotRuntime = class {
8146
8571
  );
8147
8572
  history.push({
8148
8573
  role: "assistant",
8149
- content: response.content
8574
+ content: response.content,
8575
+ frameType: "assistant"
8150
8576
  });
8151
8577
  const toolResults = [];
8578
+ let hadFatalError = false;
8152
8579
  for (const block of allowed) {
8153
8580
  try {
8154
8581
  const result = await this.config.toolExecutor(
@@ -8161,12 +8588,23 @@ var BotRuntime = class {
8161
8588
  content: result
8162
8589
  });
8163
8590
  } catch (err) {
8164
- toolResults.push({
8165
- type: "tool_result",
8166
- tool_use_id: block.id,
8167
- content: `Error: ${err instanceof Error ? err.message : String(err)}`,
8168
- is_error: true
8169
- });
8591
+ const classified = classifyError(err, block.name);
8592
+ if (classified instanceof FatalBotError) {
8593
+ toolResults.push({
8594
+ type: "tool_result",
8595
+ tool_use_id: block.id,
8596
+ content: `Fatal error: ${classified.message}`,
8597
+ is_error: true
8598
+ });
8599
+ hadFatalError = true;
8600
+ } else {
8601
+ toolResults.push({
8602
+ type: "tool_result",
8603
+ tool_use_id: block.id,
8604
+ content: `Error (recoverable): ${classified.message}`,
8605
+ is_error: true
8606
+ });
8607
+ }
8170
8608
  }
8171
8609
  }
8172
8610
  for (const { block, check } of blocked) {
@@ -8179,10 +8617,22 @@ var BotRuntime = class {
8179
8617
  }
8180
8618
  history.push({
8181
8619
  role: "user",
8182
- content: toolResults
8620
+ content: toolResults,
8621
+ frameType: hadFatalError ? "error" : "tool_result"
8183
8622
  });
8623
+ if (hadFatalError) {
8624
+ this.trimHistory(sessionKey);
8625
+ return `A fatal error occurred during tool execution. The bot loop has been stopped.`;
8626
+ }
8184
8627
  }
8185
- return "I reached the maximum number of tool calls for this request. Please try again with a more specific question.";
8628
+ const maxErr = new MaxStepsError(turns, maxTurns);
8629
+ history.push({
8630
+ role: "assistant",
8631
+ content: maxErr.message,
8632
+ frameType: "error"
8633
+ });
8634
+ this.trimHistory(sessionKey);
8635
+ return maxErr.message;
8186
8636
  }
8187
8637
  getHistory(sessionKey) {
8188
8638
  if (!this.conversations.has(sessionKey)) {
@@ -8192,9 +8642,19 @@ var BotRuntime = class {
8192
8642
  }
8193
8643
  trimHistory(sessionKey) {
8194
8644
  const history = this.conversations.get(sessionKey);
8195
- if (history && history.length > MAX_HISTORY) {
8196
- this.conversations.set(sessionKey, history.slice(-MAX_HISTORY));
8645
+ if (!history || history.length <= MAX_HISTORY) return;
8646
+ const firstTaskIdx = history.findIndex((m) => m.frameType === "task");
8647
+ let trimmed = history.filter(
8648
+ (m, i) => m.frameType !== "planning" || i >= history.length - MAX_HISTORY
8649
+ );
8650
+ if (trimmed.length > MAX_HISTORY) {
8651
+ const tail = trimmed.slice(-MAX_HISTORY);
8652
+ if (firstTaskIdx >= 0 && !tail.includes(history[firstTaskIdx])) {
8653
+ tail[0] = history[firstTaskIdx];
8654
+ }
8655
+ trimmed = tail;
8197
8656
  }
8657
+ this.conversations.set(sessionKey, trimmed);
8198
8658
  }
8199
8659
  /** Clear conversation history for a session */
8200
8660
  clearHistory(sessionKey) {
@@ -8900,8 +9360,19 @@ import { randomUUID as randomUUID5 } from "crypto";
8900
9360
  import { homedir } from "os";
8901
9361
  import { join } from "path";
8902
9362
  import { mkdirSync as mkdirSync2 } from "fs";
8903
- var RECONNECT_DELAY_MS = 5e3;
9363
+ var INITIAL_BACKOFF_MS = 1e3;
9364
+ var MAX_BACKOFF_MS = 3e5;
9365
+ var BACKOFF_MULTIPLIER = 2;
9366
+ var JITTER_FACTOR = 0.25;
8904
9367
  var AUTH_DIR = join(homedir(), ".exe-os", "whatsapp-auth");
9368
+ function calculateBackoff(retryCount) {
9369
+ const base = Math.min(
9370
+ INITIAL_BACKOFF_MS * BACKOFF_MULTIPLIER ** retryCount,
9371
+ MAX_BACKOFF_MS
9372
+ );
9373
+ const jitter = base * JITTER_FACTOR * (2 * Math.random() - 1);
9374
+ return Math.max(INITIAL_BACKOFF_MS, Math.round(base + jitter));
9375
+ }
8905
9376
  var WhatsAppAdapter = class {
8906
9377
  platform = "whatsapp";
8907
9378
  sock = null;
@@ -8909,6 +9380,9 @@ var WhatsAppAdapter = class {
8909
9380
  connected = false;
8910
9381
  abortController = null;
8911
9382
  authDir = AUTH_DIR;
9383
+ // Resilience state
9384
+ retryCount = 0;
9385
+ disconnectedAt = 0;
8912
9386
  async connect(config2) {
8913
9387
  this.authDir = config2.credentials.authDir ?? AUTH_DIR;
8914
9388
  mkdirSync2(this.authDir, { recursive: true });
@@ -8917,6 +9391,20 @@ var WhatsAppAdapter = class {
8917
9391
  const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
8918
9392
  const { version } = await fetchLatestBaileysVersion();
8919
9393
  this.abortController = new AbortController();
9394
+ let agent;
9395
+ const socksProxy = config2.credentials.socksProxy;
9396
+ if (socksProxy) {
9397
+ try {
9398
+ const modName = "socks-proxy-agent";
9399
+ const mod = await import(modName);
9400
+ const SocksProxyAgent = mod.SocksProxyAgent ?? mod.default;
9401
+ agent = new SocksProxyAgent(socksProxy);
9402
+ console.log(`[whatsapp] Using SOCKS proxy: ${socksProxy.replace(/\/\/.*@/, "//***@")}`);
9403
+ } catch {
9404
+ console.error("[whatsapp] socks-proxy-agent not installed \u2014 run: npm i socks-proxy-agent");
9405
+ throw new Error("SOCKS proxy configured but socks-proxy-agent package not installed");
9406
+ }
9407
+ }
8920
9408
  const sock = makeWASocket({
8921
9409
  auth: {
8922
9410
  creds: state.creds,
@@ -8926,7 +9414,8 @@ var WhatsAppAdapter = class {
8926
9414
  printQRInTerminal: true,
8927
9415
  browser: ["exe-os", "cli", "1.0"],
8928
9416
  syncFullHistory: false,
8929
- markOnlineOnConnect: false
9417
+ markOnlineOnConnect: false,
9418
+ ...agent ? { agent } : {}
8930
9419
  });
8931
9420
  this.sock = sock;
8932
9421
  sock.ev.on("creds.update", saveCreds);
@@ -8934,18 +9423,32 @@ var WhatsAppAdapter = class {
8934
9423
  const { connection, lastDisconnect } = update;
8935
9424
  if (connection === "close") {
8936
9425
  this.connected = false;
9426
+ if (this.disconnectedAt === 0) this.disconnectedAt = Date.now();
8937
9427
  const statusCode = lastDisconnect?.error?.output?.statusCode;
8938
9428
  const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
8939
9429
  if (shouldReconnect && !this.abortController?.signal.aborted) {
8940
- console.log(`[whatsapp] Connection closed (${statusCode}), reconnecting...`);
8941
- setTimeout(() => void this.connect(config2), RECONNECT_DELAY_MS);
9430
+ const delay2 = calculateBackoff(this.retryCount);
9431
+ this.retryCount++;
9432
+ console.log(
9433
+ `[whatsapp] Connection closed (code=${statusCode}), retry #${this.retryCount} in ${(delay2 / 1e3).toFixed(1)}s` + (socksProxy ? ` (proxy: ${socksProxy.replace(/\/\/.*@/, "//***@")})` : "")
9434
+ );
9435
+ setTimeout(() => void this.connect(config2), delay2);
8942
9436
  } else {
8943
9437
  console.log("[whatsapp] Logged out \u2014 clear auth and re-scan QR");
8944
9438
  }
8945
9439
  }
8946
9440
  if (connection === "open") {
9441
+ if (this.retryCount > 0) {
9442
+ const downtimeSec = this.disconnectedAt > 0 ? ((Date.now() - this.disconnectedAt) / 1e3).toFixed(1) : "?";
9443
+ console.log(
9444
+ `[whatsapp] Reconnected after ${this.retryCount} retries (${downtimeSec}s downtime)`
9445
+ );
9446
+ } else {
9447
+ console.log("[whatsapp] Connected via Baileys (linked device)");
9448
+ }
8947
9449
  this.connected = true;
8948
- console.log("[whatsapp] Connected via Baileys (linked device)");
9450
+ this.retryCount = 0;
9451
+ this.disconnectedAt = 0;
8949
9452
  }
8950
9453
  });
8951
9454
  sock.ev.on("messages.upsert", (upsert) => {
@@ -11032,11 +11535,11 @@ async function ensureCRMContact(info) {
11032
11535
  }
11033
11536
 
11034
11537
  // src/automation/trigger-engine.ts
11035
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
11538
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync8, existsSync as existsSync15, mkdirSync as mkdirSync9 } from "fs";
11036
11539
  import { randomUUID as randomUUID12 } from "crypto";
11037
- import path18 from "path";
11038
- import os9 from "os";
11039
- var TRIGGERS_PATH = path18.join(os9.homedir(), ".exe-os", "triggers.json");
11540
+ import path19 from "path";
11541
+ import os10 from "os";
11542
+ var TRIGGERS_PATH = path19.join(os10.homedir(), ".exe-os", "triggers.json");
11040
11543
  var GRAPH_API_VERSION = "v21.0";
11041
11544
  function substituteTemplate(template, record) {
11042
11545
  return template.replace(
@@ -11090,9 +11593,9 @@ function evaluateConditions(conditions, record) {
11090
11593
  return conditions.every((c) => evaluateCondition(c, record));
11091
11594
  }
11092
11595
  function loadTriggers(project) {
11093
- if (!existsSync14(TRIGGERS_PATH)) return [];
11596
+ if (!existsSync15(TRIGGERS_PATH)) return [];
11094
11597
  try {
11095
- const raw = readFileSync12(TRIGGERS_PATH, "utf-8");
11598
+ const raw = readFileSync13(TRIGGERS_PATH, "utf-8");
11096
11599
  const all = JSON.parse(raw);
11097
11600
  if (!Array.isArray(all)) return [];
11098
11601
  if (project) {
@@ -11390,13 +11893,16 @@ export {
11390
11893
  FULL_ACCESS,
11391
11894
  FailoverCascade,
11392
11895
  FailoverExhaustedError,
11896
+ FatalBotError,
11393
11897
  Gateway,
11394
11898
  IMessageAdapter,
11899
+ MaxStepsError,
11395
11900
  OllamaProvider,
11396
11901
  OpenAICompatProvider,
11397
11902
  READ_ONLY,
11398
11903
  READ_TOOLS,
11399
11904
  RateLimiter,
11905
+ RecoverableBotError,
11400
11906
  SessionStore,
11401
11907
  SignalAdapter,
11402
11908
  SlackAdapter,
@@ -11408,6 +11914,7 @@ export {
11408
11914
  buildExecAssistantTools,
11409
11915
  buildPermissionContext,
11410
11916
  checkToolPermission,
11917
+ classifyError,
11411
11918
  createCRMWebhookHandler,
11412
11919
  createPerson,
11413
11920
  createReceptionist,