@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
@@ -639,6 +639,7 @@ __export(employees_exports, {
639
639
  DEFAULT_COORDINATOR_TEMPLATE_NAME: () => DEFAULT_COORDINATOR_TEMPLATE_NAME,
640
640
  EMPLOYEES_PATH: () => EMPLOYEES_PATH,
641
641
  addEmployee: () => addEmployee,
642
+ baseAgentName: () => baseAgentName,
642
643
  canCoordinate: () => canCoordinate,
643
644
  getCoordinatorEmployee: () => getCoordinatorEmployee,
644
645
  getCoordinatorName: () => getCoordinatorName,
@@ -735,6 +736,14 @@ function hasRole(agentName, role) {
735
736
  const emp = getEmployee(employees, agentName);
736
737
  return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
737
738
  }
739
+ function baseAgentName(name, employees) {
740
+ const match = name.match(/^([a-zA-Z]+)\d+$/);
741
+ if (!match) return name;
742
+ const base = match[1];
743
+ const roster = employees ?? loadEmployeesSync();
744
+ if (getEmployee(roster, base)) return base;
745
+ return name;
746
+ }
738
747
  function isMultiInstance(agentName, employees) {
739
748
  const roster = employees ?? loadEmployeesSync();
740
749
  const emp = getEmployee(roster, agentName);
@@ -851,6 +860,12 @@ function getClient() {
851
860
  if (!_resilientClient) {
852
861
  throw new Error("Database client not initialized. Call initDatabase() first.");
853
862
  }
863
+ if (process.env.EXE_IS_DAEMON === "1") {
864
+ return _resilientClient;
865
+ }
866
+ if (_daemonClient && _daemonClient._isDaemonActive()) {
867
+ return _daemonClient;
868
+ }
854
869
  return _resilientClient;
855
870
  }
856
871
  function getRawClient() {
@@ -1339,6 +1354,12 @@ async function ensureSchema() {
1339
1354
  } catch {
1340
1355
  }
1341
1356
  }
1357
+ try {
1358
+ await client.execute(
1359
+ `CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(content_hash, agent_id)`
1360
+ );
1361
+ } catch {
1362
+ }
1342
1363
  await client.executeMultiple(`
1343
1364
  CREATE TABLE IF NOT EXISTS entities (
1344
1365
  id TEXT PRIMARY KEY,
@@ -1391,7 +1412,30 @@ async function ensureSchema() {
1391
1412
  entity_id TEXT NOT NULL,
1392
1413
  PRIMARY KEY (hyperedge_id, entity_id)
1393
1414
  );
1415
+
1416
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
1417
+ name,
1418
+ content=entities,
1419
+ content_rowid=rowid
1420
+ );
1421
+
1422
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ai AFTER INSERT ON entities BEGIN
1423
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1424
+ END;
1425
+
1426
+ CREATE TRIGGER IF NOT EXISTS entities_fts_ad AFTER DELETE ON entities BEGIN
1427
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1428
+ END;
1429
+
1430
+ CREATE TRIGGER IF NOT EXISTS entities_fts_au AFTER UPDATE ON entities BEGIN
1431
+ INSERT INTO entities_fts(entities_fts, rowid, name) VALUES('delete', old.rowid, old.name);
1432
+ INSERT INTO entities_fts(rowid, name) VALUES (new.rowid, new.name);
1433
+ END;
1394
1434
  `);
1435
+ try {
1436
+ await client.execute("INSERT INTO entities_fts(entities_fts) VALUES('rebuild')");
1437
+ } catch {
1438
+ }
1395
1439
  await client.executeMultiple(`
1396
1440
  CREATE TABLE IF NOT EXISTS entity_aliases (
1397
1441
  alias TEXT NOT NULL PRIMARY KEY,
@@ -1572,6 +1616,33 @@ async function ensureSchema() {
1572
1616
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1573
1617
  ON conversations(channel_id);
1574
1618
  `);
1619
+ await client.executeMultiple(`
1620
+ CREATE TABLE IF NOT EXISTS session_agent_map (
1621
+ session_uuid TEXT PRIMARY KEY,
1622
+ agent_id TEXT NOT NULL,
1623
+ session_name TEXT,
1624
+ task_id TEXT,
1625
+ project_name TEXT,
1626
+ started_at TEXT NOT NULL
1627
+ );
1628
+
1629
+ CREATE INDEX IF NOT EXISTS idx_session_agent_map_agent
1630
+ ON session_agent_map(agent_id);
1631
+ `);
1632
+ try {
1633
+ const mapCount = await client.execute({ sql: `SELECT COUNT(*) as cnt FROM session_agent_map`, args: [] });
1634
+ if (Number(mapCount.rows[0]?.cnt ?? 0) === 0) {
1635
+ await client.execute({
1636
+ sql: `INSERT OR IGNORE INTO session_agent_map (session_uuid, agent_id, session_name, started_at)
1637
+ SELECT session_id, agent_id, '', MIN(timestamp)
1638
+ FROM memories
1639
+ WHERE session_id IS NOT NULL AND session_id != '' AND agent_id IS NOT NULL AND agent_id != ''
1640
+ GROUP BY session_id, agent_id`,
1641
+ args: []
1642
+ });
1643
+ }
1644
+ } catch {
1645
+ }
1575
1646
  try {
1576
1647
  await client.execute({
1577
1648
  sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
@@ -1705,15 +1776,41 @@ async function ensureSchema() {
1705
1776
  });
1706
1777
  } catch {
1707
1778
  }
1779
+ for (const col of [
1780
+ "ALTER TABLE memories ADD COLUMN intent TEXT",
1781
+ "ALTER TABLE memories ADD COLUMN outcome TEXT",
1782
+ "ALTER TABLE memories ADD COLUMN domain TEXT",
1783
+ "ALTER TABLE memories ADD COLUMN referenced_entities TEXT",
1784
+ "ALTER TABLE memories ADD COLUMN retrieval_count INTEGER DEFAULT 0",
1785
+ "ALTER TABLE memories ADD COLUMN chain_position TEXT",
1786
+ "ALTER TABLE memories ADD COLUMN review_status TEXT",
1787
+ "ALTER TABLE memories ADD COLUMN context_window_pct INTEGER",
1788
+ "ALTER TABLE memories ADD COLUMN file_paths TEXT",
1789
+ "ALTER TABLE memories ADD COLUMN commit_hash TEXT",
1790
+ "ALTER TABLE memories ADD COLUMN duration_ms INTEGER",
1791
+ "ALTER TABLE memories ADD COLUMN token_cost REAL",
1792
+ "ALTER TABLE memories ADD COLUMN audience TEXT",
1793
+ "ALTER TABLE memories ADD COLUMN language_type TEXT",
1794
+ "ALTER TABLE memories ADD COLUMN parent_memory_id TEXT"
1795
+ ]) {
1796
+ try {
1797
+ await client.execute(col);
1798
+ } catch {
1799
+ }
1800
+ }
1708
1801
  }
1709
1802
  async function disposeDatabase() {
1803
+ if (_daemonClient) {
1804
+ _daemonClient.close();
1805
+ _daemonClient = null;
1806
+ }
1710
1807
  if (_client) {
1711
1808
  _client.close();
1712
1809
  _client = null;
1713
1810
  _resilientClient = null;
1714
1811
  }
1715
1812
  }
1716
- var _client, _resilientClient, initTurso, disposeTurso;
1813
+ var _client, _resilientClient, _daemonClient, initTurso, disposeTurso;
1717
1814
  var init_database = __esm({
1718
1815
  "src/lib/database.ts"() {
1719
1816
  "use strict";
@@ -1721,6 +1818,7 @@ var init_database = __esm({
1721
1818
  init_employees();
1722
1819
  _client = null;
1723
1820
  _resilientClient = null;
1821
+ _daemonClient = null;
1724
1822
  initTurso = initDatabase;
1725
1823
  disposeTurso = disposeDatabase;
1726
1824
  }
@@ -1755,10 +1853,12 @@ function handleData(chunk) {
1755
1853
  if (!line) continue;
1756
1854
  try {
1757
1855
  const response = JSON.parse(line);
1758
- const entry = _pending.get(response.id);
1856
+ const id = response.id;
1857
+ if (!id) continue;
1858
+ const entry = _pending.get(id);
1759
1859
  if (entry) {
1760
1860
  clearTimeout(entry.timer);
1761
- _pending.delete(response.id);
1861
+ _pending.delete(id);
1762
1862
  entry.resolve(response);
1763
1863
  }
1764
1864
  } catch {
@@ -1929,6 +2029,9 @@ async function connectEmbedDaemon() {
1929
2029
  return false;
1930
2030
  }
1931
2031
  function sendRequest(texts, priority) {
2032
+ return sendDaemonRequest({ texts, priority });
2033
+ }
2034
+ function sendDaemonRequest(payload, timeoutMs = REQUEST_TIMEOUT_MS) {
1932
2035
  return new Promise((resolve) => {
1933
2036
  if (!_socket || !_connected) {
1934
2037
  resolve({ error: "Not connected" });
@@ -1938,10 +2041,10 @@ function sendRequest(texts, priority) {
1938
2041
  const timer = setTimeout(() => {
1939
2042
  _pending.delete(id);
1940
2043
  resolve({ error: "Request timeout" });
1941
- }, REQUEST_TIMEOUT_MS);
2044
+ }, timeoutMs);
1942
2045
  _pending.set(id, { resolve, timer });
1943
2046
  try {
1944
- _socket.write(JSON.stringify({ id, texts, priority }) + "\n");
2047
+ _socket.write(JSON.stringify({ id, ...payload }) + "\n");
1945
2048
  } catch {
1946
2049
  clearTimeout(timer);
1947
2050
  _pending.delete(id);
@@ -1951,30 +2054,11 @@ function sendRequest(texts, priority) {
1951
2054
  }
1952
2055
  async function pingDaemon() {
1953
2056
  if (!_socket || !_connected) return null;
1954
- return new Promise((resolve) => {
1955
- const id = randomUUID();
1956
- const timer = setTimeout(() => {
1957
- _pending.delete(id);
1958
- resolve(null);
1959
- }, 5e3);
1960
- _pending.set(id, {
1961
- resolve: (data) => {
1962
- if (data.health) {
1963
- resolve(data.health);
1964
- } else {
1965
- resolve(null);
1966
- }
1967
- },
1968
- timer
1969
- });
1970
- try {
1971
- _socket.write(JSON.stringify({ id, type: "health" }) + "\n");
1972
- } catch {
1973
- clearTimeout(timer);
1974
- _pending.delete(id);
1975
- resolve(null);
1976
- }
1977
- });
2057
+ const response = await sendDaemonRequest({ type: "health" }, 5e3);
2058
+ if (response.health) {
2059
+ return response.health;
2060
+ }
2061
+ return null;
1978
2062
  }
1979
2063
  function killAndRespawnDaemon() {
1980
2064
  process.stderr.write("[exed-client] Killing daemon for restart...\n");
@@ -2117,10 +2201,10 @@ async function disposeEmbedder() {
2117
2201
  async function embedDirect(text) {
2118
2202
  const llamaCpp = await import("node-llama-cpp");
2119
2203
  const { MODELS_DIR: MODELS_DIR2 } = await Promise.resolve().then(() => (init_config(), config_exports));
2120
- const { existsSync: existsSync16 } = await import("fs");
2121
- const path20 = await import("path");
2122
- const modelPath = path20.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
2123
- if (!existsSync16(modelPath)) {
2204
+ const { existsSync: existsSync17 } = await import("fs");
2205
+ const path21 = await import("path");
2206
+ const modelPath = path21.join(MODELS_DIR2, "jina-embeddings-v5-small-q4_k_m.gguf");
2207
+ if (!existsSync17(modelPath)) {
2124
2208
  throw new Error(`Embedding model not found at ${modelPath}. Run '/exe-setup' to download it.`);
2125
2209
  }
2126
2210
  const llama = await llamaCpp.getLlama();
@@ -2179,12 +2263,20 @@ async function getMasterKey() {
2179
2263
  }
2180
2264
  const keyPath = getKeyPath();
2181
2265
  if (!existsSync4(keyPath)) {
2266
+ process.stderr.write(
2267
+ `[keychain] Key not found at ${keyPath} (HOME=${os3.homedir()}, EXE_OS_DIR=${process.env.EXE_OS_DIR ?? "unset"})
2268
+ `
2269
+ );
2182
2270
  return null;
2183
2271
  }
2184
2272
  try {
2185
2273
  const content = await readFile3(keyPath, "utf-8");
2186
2274
  return Buffer.from(content.trim(), "base64");
2187
- } catch {
2275
+ } catch (err) {
2276
+ process.stderr.write(
2277
+ `[keychain] Key read failed at ${keyPath}: ${err instanceof Error ? err.message : String(err)}
2278
+ `
2279
+ );
2188
2280
  return null;
2189
2281
  }
2190
2282
  }
@@ -2640,6 +2732,7 @@ __export(store_exports, {
2640
2732
  vectorToBlob: () => vectorToBlob,
2641
2733
  writeMemory: () => writeMemory
2642
2734
  });
2735
+ import { createHash } from "crypto";
2643
2736
  function isBusyError2(err) {
2644
2737
  if (err instanceof Error) {
2645
2738
  const msg = err.message.toLowerCase();
@@ -2713,12 +2806,52 @@ function classifyTier(record) {
2713
2806
  if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
2714
2807
  return 3;
2715
2808
  }
2809
+ function inferFilePaths(record) {
2810
+ if (!["Read", "Write", "Edit"].includes(record.tool_name)) return null;
2811
+ const firstLine = record.raw_text.split("\n")[0] ?? "";
2812
+ const match = firstLine.match(/(\/[\w./-]+\.\w+)/);
2813
+ return match ? JSON.stringify([match[1]]) : null;
2814
+ }
2815
+ function inferCommitHash(record) {
2816
+ if (record.tool_name !== "Bash") return null;
2817
+ const match = record.raw_text.match(/\b([a-f0-9]{7,40})\b/);
2818
+ return match ? match[1] : null;
2819
+ }
2820
+ function inferLanguageType(record) {
2821
+ const text = record.raw_text;
2822
+ if (!text || text.length < 10) return null;
2823
+ const trimmed = text.trimStart();
2824
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
2825
+ if (/\b(SELECT|INSERT|UPDATE|DELETE|CREATE TABLE|ALTER TABLE)\b/i.test(text)) return "sql";
2826
+ if (/\b(function |const |import |export |class |def |async |=>)\b/.test(text)) return "code";
2827
+ if (trimmed.startsWith("#") || trimmed.startsWith("*")) return "prose";
2828
+ return "mixed";
2829
+ }
2830
+ function inferDomain(record) {
2831
+ const proj = (record.project_name ?? "").toLowerCase();
2832
+ if (proj.includes("marketing") || proj.includes("content")) return "marketing";
2833
+ if (proj.includes("crm") || proj.includes("customer")) return "customer";
2834
+ return null;
2835
+ }
2716
2836
  async function writeMemory(record) {
2717
2837
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
2718
2838
  throw new Error(
2719
2839
  `Expected ${EMBEDDING_DIM}-dim vector, got ${record.vector.length}`
2720
2840
  );
2721
2841
  }
2842
+ const contentHash = createHash("md5").update(record.raw_text).digest("hex");
2843
+ if (_pendingRecords.some((r) => r.content_hash === contentHash && r.agent_id === record.agent_id)) {
2844
+ return;
2845
+ }
2846
+ try {
2847
+ const client = getClient();
2848
+ const existing = await client.execute({
2849
+ sql: "SELECT id FROM memories WHERE content_hash = ? AND agent_id = ? LIMIT 1",
2850
+ args: [contentHash, record.agent_id]
2851
+ });
2852
+ if (existing.rows.length > 0) return;
2853
+ } catch {
2854
+ }
2722
2855
  const dbRow = {
2723
2856
  id: record.id,
2724
2857
  agent_id: record.agent_id,
@@ -2748,7 +2881,23 @@ async function writeMemory(record) {
2748
2881
  supersedes_id: record.supersedes_id ?? null,
2749
2882
  draft: record.draft ? 1 : 0,
2750
2883
  memory_type: record.memory_type ?? "raw",
2751
- trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null
2884
+ trajectory: record.trajectory ? JSON.stringify(record.trajectory) : null,
2885
+ content_hash: contentHash,
2886
+ intent: record.intent ?? null,
2887
+ outcome: record.outcome ?? null,
2888
+ domain: record.domain ?? inferDomain(record),
2889
+ referenced_entities: record.referenced_entities ?? null,
2890
+ retrieval_count: record.retrieval_count ?? 0,
2891
+ chain_position: record.chain_position ?? null,
2892
+ review_status: record.review_status ?? null,
2893
+ context_window_pct: record.context_window_pct ?? null,
2894
+ file_paths: record.file_paths ?? inferFilePaths(record),
2895
+ commit_hash: record.commit_hash ?? inferCommitHash(record),
2896
+ duration_ms: record.duration_ms ?? null,
2897
+ token_cost: record.token_cost ?? null,
2898
+ audience: record.audience ?? null,
2899
+ language_type: record.language_type ?? inferLanguageType(record),
2900
+ parent_memory_id: record.parent_memory_id ?? null
2752
2901
  };
2753
2902
  _pendingRecords.push(dbRow);
2754
2903
  orgBus.emit({
@@ -2806,80 +2955,85 @@ async function flushBatch() {
2806
2955
  const draft = row.draft ? 1 : 0;
2807
2956
  const memoryType = row.memory_type ?? "raw";
2808
2957
  const trajectory = row.trajectory ?? null;
2809
- return {
2810
- sql: hasVector ? `INSERT OR IGNORE INTO memories
2811
- (id, agent_id, agent_role, session_id, timestamp,
2958
+ const contentHash = row.content_hash ?? null;
2959
+ const intent = row.intent ?? null;
2960
+ const outcome = row.outcome ?? null;
2961
+ const domain = row.domain ?? null;
2962
+ const referencedEntities = row.referenced_entities ?? null;
2963
+ const retrievalCount = row.retrieval_count ?? 0;
2964
+ const chainPosition = row.chain_position ?? null;
2965
+ const reviewStatus = row.review_status ?? null;
2966
+ const contextWindowPct = row.context_window_pct ?? null;
2967
+ const filePaths = row.file_paths ?? null;
2968
+ const commitHash = row.commit_hash ?? null;
2969
+ const durationMs = row.duration_ms ?? null;
2970
+ const tokenCost = row.token_cost ?? null;
2971
+ const audience = row.audience ?? null;
2972
+ const languageType = row.language_type ?? null;
2973
+ const parentMemoryId = row.parent_memory_id ?? null;
2974
+ const cols = `id, agent_id, agent_role, session_id, timestamp,
2812
2975
  tool_name, project_name,
2813
2976
  has_error, raw_text, vector, version, task_id, importance, status,
2814
2977
  confidence, last_accessed,
2815
2978
  workspace_id, document_id, user_id, char_offset, page_number,
2816
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2817
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
2818
- (id, agent_id, agent_role, session_id, timestamp,
2819
- tool_name, project_name,
2820
- has_error, raw_text, vector, version, task_id, importance, status,
2821
- confidence, last_accessed,
2822
- workspace_id, document_id, user_id, char_offset, page_number,
2823
- source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory)
2824
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2825
- args: hasVector ? [
2826
- row.id,
2827
- row.agent_id,
2828
- row.agent_role,
2829
- row.session_id,
2830
- row.timestamp,
2831
- row.tool_name,
2832
- row.project_name,
2833
- row.has_error,
2834
- row.raw_text,
2835
- vectorToBlob(row.vector),
2836
- row.version,
2837
- taskId,
2838
- importance,
2839
- status,
2840
- confidence,
2841
- lastAccessed,
2842
- workspaceId,
2843
- documentId,
2844
- userId,
2845
- charOffset,
2846
- pageNumber,
2847
- sourcePath,
2848
- sourceType,
2849
- tier,
2850
- supersedesId,
2851
- draft,
2852
- memoryType,
2853
- trajectory
2854
- ] : [
2855
- row.id,
2856
- row.agent_id,
2857
- row.agent_role,
2858
- row.session_id,
2859
- row.timestamp,
2860
- row.tool_name,
2861
- row.project_name,
2862
- row.has_error,
2863
- row.raw_text,
2864
- row.version,
2865
- taskId,
2866
- importance,
2867
- status,
2868
- confidence,
2869
- lastAccessed,
2870
- workspaceId,
2871
- documentId,
2872
- userId,
2873
- charOffset,
2874
- pageNumber,
2875
- sourcePath,
2876
- sourceType,
2877
- tier,
2878
- supersedesId,
2879
- draft,
2880
- memoryType,
2881
- trajectory
2882
- ]
2979
+ source_path, source_type, tier, supersedes_id, draft, memory_type, trajectory, content_hash,
2980
+ intent, outcome, domain, referenced_entities, retrieval_count,
2981
+ chain_position, review_status, context_window_pct, file_paths, commit_hash,
2982
+ duration_ms, token_cost, audience, language_type, parent_memory_id`;
2983
+ const metaArgs = [
2984
+ intent,
2985
+ outcome,
2986
+ domain,
2987
+ referencedEntities,
2988
+ retrievalCount,
2989
+ chainPosition,
2990
+ reviewStatus,
2991
+ contextWindowPct,
2992
+ filePaths,
2993
+ commitHash,
2994
+ durationMs,
2995
+ tokenCost,
2996
+ audience,
2997
+ languageType,
2998
+ parentMemoryId
2999
+ ];
3000
+ const baseArgs = [
3001
+ row.id,
3002
+ row.agent_id,
3003
+ row.agent_role,
3004
+ row.session_id,
3005
+ row.timestamp,
3006
+ row.tool_name,
3007
+ row.project_name,
3008
+ row.has_error,
3009
+ row.raw_text
3010
+ ];
3011
+ const sharedArgs = [
3012
+ row.version,
3013
+ taskId,
3014
+ importance,
3015
+ status,
3016
+ confidence,
3017
+ lastAccessed,
3018
+ workspaceId,
3019
+ documentId,
3020
+ userId,
3021
+ charOffset,
3022
+ pageNumber,
3023
+ sourcePath,
3024
+ sourceType,
3025
+ tier,
3026
+ supersedesId,
3027
+ draft,
3028
+ memoryType,
3029
+ trajectory,
3030
+ contentHash
3031
+ ];
3032
+ return {
3033
+ sql: hasVector ? `INSERT OR IGNORE INTO memories (${cols})
3034
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories (${cols})
3035
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3036
+ args: hasVector ? [...baseArgs, vectorToBlob(row.vector), ...sharedArgs, ...metaArgs] : [...baseArgs, ...sharedArgs, ...metaArgs]
2883
3037
  };
2884
3038
  };
2885
3039
  const globalClient = getClient();
@@ -3138,8 +3292,8 @@ __export(wiki_client_exports, {
3138
3292
  listDocuments: () => listDocuments,
3139
3293
  listWorkspaces: () => listWorkspaces
3140
3294
  });
3141
- async function wikiFetch(config2, path20, method = "GET", body) {
3142
- const url = `${config2.baseUrl}/api/v1${path20}`;
3295
+ async function wikiFetch(config2, path21, method = "GET", body) {
3296
+ const url = `${config2.baseUrl}/api/v1${path21}`;
3143
3297
  const headers = {
3144
3298
  Authorization: `Bearer ${config2.apiKey}`,
3145
3299
  "Content-Type": "application/json"
@@ -3172,7 +3326,7 @@ async function wikiFetch(config2, path20, method = "GET", body) {
3172
3326
  }
3173
3327
  }
3174
3328
  if (!response.ok) {
3175
- throw new Error(`Wiki API ${method} ${path20}: ${response.status} ${response.statusText}`);
3329
+ throw new Error(`Wiki API ${method} ${path21}: ${response.status} ${response.statusText}`);
3176
3330
  }
3177
3331
  return response.json();
3178
3332
  } finally {
@@ -3266,7 +3420,7 @@ var LOCAL_WIKI_URL, REQUEST_TIMEOUT_MS2;
3266
3420
  var init_wiki_client = __esm({
3267
3421
  "src/lib/wiki-client.ts"() {
3268
3422
  "use strict";
3269
- LOCAL_WIKI_URL = "http://localhost:3001";
3423
+ LOCAL_WIKI_URL = process.env.EXE_WIKI_URL || "http://localhost:3001";
3270
3424
  REQUEST_TIMEOUT_MS2 = 8e3;
3271
3425
  }
3272
3426
  });
@@ -4574,17 +4728,29 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeHztAMOpR/ZMh+rWuOASjEZ54CGY
4574
4728
  // src/gateway/adapters/whatsapp.ts
4575
4729
  var whatsapp_exports = {};
4576
4730
  __export(whatsapp_exports, {
4577
- WhatsAppAdapter: () => WhatsAppAdapter
4731
+ WhatsAppAdapter: () => WhatsAppAdapter,
4732
+ calculateBackoff: () => calculateBackoff
4578
4733
  });
4579
4734
  import { randomUUID as randomUUID4 } from "crypto";
4580
4735
  import { homedir } from "os";
4581
4736
  import { join } from "path";
4582
4737
  import { mkdirSync as mkdirSync3 } from "fs";
4583
- var RECONNECT_DELAY_MS, AUTH_DIR, WhatsAppAdapter;
4738
+ function calculateBackoff(retryCount) {
4739
+ const base = Math.min(
4740
+ INITIAL_BACKOFF_MS * BACKOFF_MULTIPLIER ** retryCount,
4741
+ MAX_BACKOFF_MS
4742
+ );
4743
+ const jitter = base * JITTER_FACTOR * (2 * Math.random() - 1);
4744
+ return Math.max(INITIAL_BACKOFF_MS, Math.round(base + jitter));
4745
+ }
4746
+ var INITIAL_BACKOFF_MS, MAX_BACKOFF_MS, BACKOFF_MULTIPLIER, JITTER_FACTOR, AUTH_DIR, WhatsAppAdapter;
4584
4747
  var init_whatsapp = __esm({
4585
4748
  "src/gateway/adapters/whatsapp.ts"() {
4586
4749
  "use strict";
4587
- RECONNECT_DELAY_MS = 5e3;
4750
+ INITIAL_BACKOFF_MS = 1e3;
4751
+ MAX_BACKOFF_MS = 3e5;
4752
+ BACKOFF_MULTIPLIER = 2;
4753
+ JITTER_FACTOR = 0.25;
4588
4754
  AUTH_DIR = join(homedir(), ".exe-os", "whatsapp-auth");
4589
4755
  WhatsAppAdapter = class {
4590
4756
  platform = "whatsapp";
@@ -4593,6 +4759,9 @@ var init_whatsapp = __esm({
4593
4759
  connected = false;
4594
4760
  abortController = null;
4595
4761
  authDir = AUTH_DIR;
4762
+ // Resilience state
4763
+ retryCount = 0;
4764
+ disconnectedAt = 0;
4596
4765
  async connect(config2) {
4597
4766
  this.authDir = config2.credentials.authDir ?? AUTH_DIR;
4598
4767
  mkdirSync3(this.authDir, { recursive: true });
@@ -4601,6 +4770,20 @@ var init_whatsapp = __esm({
4601
4770
  const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
4602
4771
  const { version } = await fetchLatestBaileysVersion();
4603
4772
  this.abortController = new AbortController();
4773
+ let agent;
4774
+ const socksProxy = config2.credentials.socksProxy;
4775
+ if (socksProxy) {
4776
+ try {
4777
+ const modName = "socks-proxy-agent";
4778
+ const mod = await import(modName);
4779
+ const SocksProxyAgent = mod.SocksProxyAgent ?? mod.default;
4780
+ agent = new SocksProxyAgent(socksProxy);
4781
+ console.log(`[whatsapp] Using SOCKS proxy: ${socksProxy.replace(/\/\/.*@/, "//***@")}`);
4782
+ } catch {
4783
+ console.error("[whatsapp] socks-proxy-agent not installed \u2014 run: npm i socks-proxy-agent");
4784
+ throw new Error("SOCKS proxy configured but socks-proxy-agent package not installed");
4785
+ }
4786
+ }
4604
4787
  const sock = makeWASocket({
4605
4788
  auth: {
4606
4789
  creds: state.creds,
@@ -4610,7 +4793,8 @@ var init_whatsapp = __esm({
4610
4793
  printQRInTerminal: true,
4611
4794
  browser: ["exe-os", "cli", "1.0"],
4612
4795
  syncFullHistory: false,
4613
- markOnlineOnConnect: false
4796
+ markOnlineOnConnect: false,
4797
+ ...agent ? { agent } : {}
4614
4798
  });
4615
4799
  this.sock = sock;
4616
4800
  sock.ev.on("creds.update", saveCreds);
@@ -4618,18 +4802,32 @@ var init_whatsapp = __esm({
4618
4802
  const { connection, lastDisconnect } = update;
4619
4803
  if (connection === "close") {
4620
4804
  this.connected = false;
4805
+ if (this.disconnectedAt === 0) this.disconnectedAt = Date.now();
4621
4806
  const statusCode = lastDisconnect?.error?.output?.statusCode;
4622
4807
  const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
4623
4808
  if (shouldReconnect && !this.abortController?.signal.aborted) {
4624
- console.log(`[whatsapp] Connection closed (${statusCode}), reconnecting...`);
4625
- setTimeout(() => void this.connect(config2), RECONNECT_DELAY_MS);
4809
+ const delay2 = calculateBackoff(this.retryCount);
4810
+ this.retryCount++;
4811
+ console.log(
4812
+ `[whatsapp] Connection closed (code=${statusCode}), retry #${this.retryCount} in ${(delay2 / 1e3).toFixed(1)}s` + (socksProxy ? ` (proxy: ${socksProxy.replace(/\/\/.*@/, "//***@")})` : "")
4813
+ );
4814
+ setTimeout(() => void this.connect(config2), delay2);
4626
4815
  } else {
4627
4816
  console.log("[whatsapp] Logged out \u2014 clear auth and re-scan QR");
4628
4817
  }
4629
4818
  }
4630
4819
  if (connection === "open") {
4820
+ if (this.retryCount > 0) {
4821
+ const downtimeSec = this.disconnectedAt > 0 ? ((Date.now() - this.disconnectedAt) / 1e3).toFixed(1) : "?";
4822
+ console.log(
4823
+ `[whatsapp] Reconnected after ${this.retryCount} retries (${downtimeSec}s downtime)`
4824
+ );
4825
+ } else {
4826
+ console.log("[whatsapp] Connected via Baileys (linked device)");
4827
+ }
4631
4828
  this.connected = true;
4632
- console.log("[whatsapp] Connected via Baileys (linked device)");
4829
+ this.retryCount = 0;
4830
+ this.disconnectedAt = 0;
4633
4831
  }
4634
4832
  });
4635
4833
  sock.ev.on("messages.upsert", (upsert) => {
@@ -5718,9 +5916,9 @@ __export(webhook_exports, {
5718
5916
  WebhookAdapter: () => WebhookAdapter
5719
5917
  });
5720
5918
  import { randomUUID as randomUUID7 } from "crypto";
5721
- function resolvePath(obj, path20) {
5919
+ function resolvePath(obj, path21) {
5722
5920
  let current = obj;
5723
- for (const segment of path20.split(".")) {
5921
+ for (const segment of path21.split(".")) {
5724
5922
  if (current == null || typeof current !== "object") return void 0;
5725
5923
  current = current[segment];
5726
5924
  }
@@ -6117,18 +6315,69 @@ var init_provider_table = __esm({
6117
6315
  }
6118
6316
  });
6119
6317
 
6120
- // src/lib/intercom-queue.ts
6121
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, renameSync as renameSync3, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
6318
+ // src/lib/runtime-table.ts
6319
+ var RUNTIME_TABLE, DEFAULT_RUNTIME;
6320
+ var init_runtime_table = __esm({
6321
+ "src/lib/runtime-table.ts"() {
6322
+ "use strict";
6323
+ RUNTIME_TABLE = {
6324
+ codex: {
6325
+ binary: "codex",
6326
+ launchMode: "exec",
6327
+ autoApproveFlag: "--full-auto",
6328
+ inlineFlag: "--no-alt-screen",
6329
+ apiKeyEnv: "OPENAI_API_KEY",
6330
+ defaultModel: "gpt-5.4"
6331
+ }
6332
+ };
6333
+ DEFAULT_RUNTIME = "claude";
6334
+ }
6335
+ });
6336
+
6337
+ // src/lib/agent-config.ts
6338
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
6122
6339
  import path9 from "path";
6340
+ function loadAgentConfig() {
6341
+ if (!existsSync8(AGENT_CONFIG_PATH)) return {};
6342
+ try {
6343
+ return JSON.parse(readFileSync7(AGENT_CONFIG_PATH, "utf-8"));
6344
+ } catch {
6345
+ return {};
6346
+ }
6347
+ }
6348
+ function getAgentRuntime(agentId) {
6349
+ const config2 = loadAgentConfig();
6350
+ const entry = config2[agentId];
6351
+ if (entry) return entry;
6352
+ return { runtime: DEFAULT_RUNTIME, model: DEFAULT_MODELS[DEFAULT_RUNTIME] };
6353
+ }
6354
+ var AGENT_CONFIG_PATH, DEFAULT_MODELS;
6355
+ var init_agent_config = __esm({
6356
+ "src/lib/agent-config.ts"() {
6357
+ "use strict";
6358
+ init_config();
6359
+ init_runtime_table();
6360
+ AGENT_CONFIG_PATH = path9.join(EXE_AI_DIR, "agent-config.json");
6361
+ DEFAULT_MODELS = {
6362
+ claude: "claude-opus-4",
6363
+ codex: RUNTIME_TABLE.codex?.defaultModel ?? "gpt-5.4",
6364
+ opencode: "minimax-m2.7"
6365
+ };
6366
+ }
6367
+ });
6368
+
6369
+ // src/lib/intercom-queue.ts
6370
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, renameSync as renameSync3, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
6371
+ import path10 from "path";
6123
6372
  import os6 from "os";
6124
6373
  function ensureDir() {
6125
- const dir = path9.dirname(QUEUE_PATH);
6126
- if (!existsSync8(dir)) mkdirSync5(dir, { recursive: true });
6374
+ const dir = path10.dirname(QUEUE_PATH);
6375
+ if (!existsSync9(dir)) mkdirSync6(dir, { recursive: true });
6127
6376
  }
6128
6377
  function readQueue() {
6129
6378
  try {
6130
- if (!existsSync8(QUEUE_PATH)) return [];
6131
- return JSON.parse(readFileSync7(QUEUE_PATH, "utf8"));
6379
+ if (!existsSync9(QUEUE_PATH)) return [];
6380
+ return JSON.parse(readFileSync8(QUEUE_PATH, "utf8"));
6132
6381
  } catch {
6133
6382
  return [];
6134
6383
  }
@@ -6136,7 +6385,7 @@ function readQueue() {
6136
6385
  function writeQueue(queue) {
6137
6386
  ensureDir();
6138
6387
  const tmp = `${QUEUE_PATH}.tmp`;
6139
- writeFileSync4(tmp, JSON.stringify(queue, null, 2));
6388
+ writeFileSync5(tmp, JSON.stringify(queue, null, 2));
6140
6389
  renameSync3(tmp, QUEUE_PATH);
6141
6390
  }
6142
6391
  function queueIntercom(targetSession, reason) {
@@ -6160,19 +6409,19 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
6160
6409
  var init_intercom_queue = __esm({
6161
6410
  "src/lib/intercom-queue.ts"() {
6162
6411
  "use strict";
6163
- QUEUE_PATH = path9.join(os6.homedir(), ".exe-os", "intercom-queue.json");
6412
+ QUEUE_PATH = path10.join(os6.homedir(), ".exe-os", "intercom-queue.json");
6164
6413
  TTL_MS = 60 * 60 * 1e3;
6165
- INTERCOM_LOG = path9.join(os6.homedir(), ".exe-os", "intercom.log");
6414
+ INTERCOM_LOG = path10.join(os6.homedir(), ".exe-os", "intercom.log");
6166
6415
  }
6167
6416
  });
6168
6417
 
6169
6418
  // src/lib/plan-limits.ts
6170
- import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
6171
- import path10 from "path";
6419
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
6420
+ import path11 from "path";
6172
6421
  function getLicenseSync() {
6173
6422
  try {
6174
- if (!existsSync9(CACHE_PATH2)) return freeLicense();
6175
- const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
6423
+ if (!existsSync10(CACHE_PATH2)) return freeLicense();
6424
+ const raw = JSON.parse(readFileSync9(CACHE_PATH2, "utf8"));
6176
6425
  if (!raw.token || typeof raw.token !== "string") return freeLicense();
6177
6426
  const parts = raw.token.split(".");
6178
6427
  if (parts.length !== 3) return freeLicense();
@@ -6210,8 +6459,8 @@ function assertEmployeeLimitSync(rosterPath) {
6210
6459
  const filePath = rosterPath ?? EMPLOYEES_PATH;
6211
6460
  let count = 0;
6212
6461
  try {
6213
- if (existsSync9(filePath)) {
6214
- const raw = readFileSync8(filePath, "utf8");
6462
+ if (existsSync10(filePath)) {
6463
+ const raw = readFileSync9(filePath, "utf8");
6215
6464
  const employees = JSON.parse(raw);
6216
6465
  count = Array.isArray(employees) ? employees.length : 0;
6217
6466
  }
@@ -6240,19 +6489,19 @@ var init_plan_limits = __esm({
6240
6489
  this.name = "PlanLimitError";
6241
6490
  }
6242
6491
  };
6243
- CACHE_PATH2 = path10.join(EXE_AI_DIR, "license-cache.json");
6492
+ CACHE_PATH2 = path11.join(EXE_AI_DIR, "license-cache.json");
6244
6493
  }
6245
6494
  });
6246
6495
 
6247
6496
  // src/lib/notifications.ts
6248
6497
  import crypto3 from "crypto";
6249
- import path11 from "path";
6498
+ import path12 from "path";
6250
6499
  import os7 from "os";
6251
6500
  import {
6252
- readFileSync as readFileSync9,
6501
+ readFileSync as readFileSync10,
6253
6502
  readdirSync as readdirSync2,
6254
6503
  unlinkSync as unlinkSync3,
6255
- existsSync as existsSync10,
6504
+ existsSync as existsSync11,
6256
6505
  rmdirSync
6257
6506
  } from "fs";
6258
6507
  async function writeNotification(notification) {
@@ -6356,10 +6605,11 @@ var init_task_scope = __esm({
6356
6605
 
6357
6606
  // src/lib/tasks-crud.ts
6358
6607
  import crypto5 from "crypto";
6359
- import path12 from "path";
6608
+ import path13 from "path";
6609
+ import os8 from "os";
6360
6610
  import { execSync as execSync4 } from "child_process";
6361
6611
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
6362
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
6612
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
6363
6613
  async function writeCheckpoint(input) {
6364
6614
  const client = getClient();
6365
6615
  const row = await resolveTask(client, input.taskId);
@@ -6400,6 +6650,35 @@ function extractParentFromContext(contextBody) {
6400
6650
  function slugify(title) {
6401
6651
  return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
6402
6652
  }
6653
+ function buildKeywordIndex() {
6654
+ const idx = /* @__PURE__ */ new Map();
6655
+ for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
6656
+ for (const kw of keywords) {
6657
+ const existing = idx.get(kw) ?? [];
6658
+ existing.push(role);
6659
+ idx.set(kw, existing);
6660
+ }
6661
+ }
6662
+ return idx;
6663
+ }
6664
+ function checkLaneAffinity(title, context, assigneeName) {
6665
+ const employees = loadEmployeesSync();
6666
+ const employee = employees.find((e) => e.name === assigneeName);
6667
+ if (!employee) return void 0;
6668
+ const assigneeRole = employee.role;
6669
+ const text = `${title} ${context}`.toLowerCase();
6670
+ const matchedRoles = /* @__PURE__ */ new Set();
6671
+ for (const [keyword, roles] of KEYWORD_INDEX) {
6672
+ if (text.includes(keyword)) {
6673
+ for (const role of roles) matchedRoles.add(role);
6674
+ }
6675
+ }
6676
+ if (matchedRoles.size === 0) return void 0;
6677
+ if (matchedRoles.has(assigneeRole)) return void 0;
6678
+ if (assigneeRole === "COO") return void 0;
6679
+ const expectedRoles = Array.from(matchedRoles).join(" or ");
6680
+ return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
6681
+ }
6403
6682
  async function resolveTask(client, identifier, scopeSession) {
6404
6683
  const scope = sessionScopeFilter(scopeSession);
6405
6684
  let result = await client.execute({
@@ -6449,7 +6728,14 @@ async function createTaskCore(input) {
6449
6728
  const id = crypto5.randomUUID();
6450
6729
  const now = (/* @__PURE__ */ new Date()).toISOString();
6451
6730
  const slug = slugify(input.title);
6452
- const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
6731
+ let earlySessionScope = null;
6732
+ try {
6733
+ const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6734
+ earlySessionScope = resolveExeSession2();
6735
+ } catch {
6736
+ }
6737
+ const scope = earlySessionScope ?? "default";
6738
+ const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
6453
6739
  let blockedById = null;
6454
6740
  const initialStatus = input.blockedBy ? "blocked" : "open";
6455
6741
  if (input.blockedBy) {
@@ -6489,22 +6775,24 @@ async function createTaskCore(input) {
6489
6775
  if (dupCheck.rows.length > 0) {
6490
6776
  warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
6491
6777
  }
6778
+ if (!process.env.DISABLE_LANE_AFFINITY) {
6779
+ const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
6780
+ if (laneWarning) {
6781
+ warning = warning ? `${warning}
6782
+ ${laneWarning}` : laneWarning;
6783
+ }
6784
+ }
6492
6785
  if (input.baseDir) {
6493
6786
  try {
6494
- await mkdir4(path12.join(input.baseDir, "exe", "output"), { recursive: true });
6495
- await mkdir4(path12.join(input.baseDir, "exe", "research"), { recursive: true });
6787
+ await mkdir4(path13.join(input.baseDir, "exe", "output"), { recursive: true });
6788
+ await mkdir4(path13.join(input.baseDir, "exe", "research"), { recursive: true });
6496
6789
  await ensureArchitectureDoc(input.baseDir, input.projectName);
6497
6790
  await ensureGitignoreExe(input.baseDir);
6498
6791
  } catch {
6499
6792
  }
6500
6793
  }
6501
6794
  const complexity = input.complexity ?? "standard";
6502
- let sessionScope = null;
6503
- try {
6504
- const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
6505
- sessionScope = resolveExeSession2();
6506
- } catch {
6507
- }
6795
+ const sessionScope = earlySessionScope;
6508
6796
  await client.execute({
6509
6797
  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)
6510
6798
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -6531,6 +6819,43 @@ async function createTaskCore(input) {
6531
6819
  now
6532
6820
  ]
6533
6821
  });
6822
+ if (input.baseDir) {
6823
+ try {
6824
+ const EXE_OS_DIR = path13.join(os8.homedir(), ".exe-os");
6825
+ const mdPath = path13.join(EXE_OS_DIR, taskFile);
6826
+ const mdDir = path13.dirname(mdPath);
6827
+ if (!existsSync12(mdDir)) await mkdir4(mdDir, { recursive: true });
6828
+ const reviewer = input.reviewer ?? input.assignedBy;
6829
+ const mdContent = `# ${input.title}
6830
+
6831
+ **ID:** ${id}
6832
+ **Status:** ${initialStatus}
6833
+ **Priority:** ${input.priority}
6834
+ **Assigned by:** ${input.assignedBy}
6835
+ **Assigned to:** ${input.assignedTo}
6836
+ **Project:** ${input.projectName}
6837
+ **Created:** ${now.split("T")[0]}${parentTaskId ? `
6838
+ **Parent task:** ${parentTaskId}` : ""}
6839
+ **Reviewer:** ${reviewer}
6840
+
6841
+ ## Context
6842
+
6843
+ ${input.context}
6844
+
6845
+ ## MANDATORY: When done
6846
+
6847
+ You MUST call update_task with status "done" and a result summary when finished.
6848
+ If you skip this, your reviewer will not know you're done and your work won't be reviewed.
6849
+ Do NOT let a failed commit or any error prevent you from calling update_task(done).
6850
+ `;
6851
+ await writeFile4(mdPath, mdContent, "utf-8");
6852
+ } catch (err) {
6853
+ process.stderr.write(
6854
+ `[create-task] WARNING: .md file write failed for ${taskFile}: ${err instanceof Error ? err.message : String(err)}
6855
+ `
6856
+ );
6857
+ }
6858
+ }
6534
6859
  return {
6535
6860
  id,
6536
6861
  title: input.title,
@@ -6723,7 +7048,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
6723
7048
  return { row, taskFile, now, taskId };
6724
7049
  }
6725
7050
  }
6726
- if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId === "exe")) {
7051
+ if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
6727
7052
  process.stderr.write(
6728
7053
  `[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
6729
7054
  `
@@ -6788,9 +7113,9 @@ async function deleteTaskCore(taskId, _baseDir) {
6788
7113
  return { taskFile, assignedTo, assignedBy, taskSlug };
6789
7114
  }
6790
7115
  async function ensureArchitectureDoc(baseDir, projectName) {
6791
- const archPath = path12.join(baseDir, "exe", "ARCHITECTURE.md");
7116
+ const archPath = path13.join(baseDir, "exe", "ARCHITECTURE.md");
6792
7117
  try {
6793
- if (existsSync11(archPath)) return;
7118
+ if (existsSync12(archPath)) return;
6794
7119
  const template = [
6795
7120
  `# ${projectName} \u2014 System Architecture`,
6796
7121
  "",
@@ -6823,10 +7148,10 @@ async function ensureArchitectureDoc(baseDir, projectName) {
6823
7148
  }
6824
7149
  }
6825
7150
  async function ensureGitignoreExe(baseDir) {
6826
- const gitignorePath = path12.join(baseDir, ".gitignore");
7151
+ const gitignorePath = path13.join(baseDir, ".gitignore");
6827
7152
  try {
6828
- if (existsSync11(gitignorePath)) {
6829
- const content = readFileSync10(gitignorePath, "utf-8");
7153
+ if (existsSync12(gitignorePath)) {
7154
+ const content = readFileSync11(gitignorePath, "utf-8");
6830
7155
  if (/^\/?exe\/?$/m.test(content)) return;
6831
7156
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
6832
7157
  } else {
@@ -6835,20 +7160,30 @@ async function ensureGitignoreExe(baseDir) {
6835
7160
  } catch {
6836
7161
  }
6837
7162
  }
6838
- var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
7163
+ var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
6839
7164
  var init_tasks_crud = __esm({
6840
7165
  "src/lib/tasks-crud.ts"() {
6841
7166
  "use strict";
6842
7167
  init_database();
6843
7168
  init_task_scope();
7169
+ init_employees();
7170
+ LANE_KEYWORDS = {
7171
+ CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
7172
+ CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
7173
+ "Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
7174
+ "Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
7175
+ "Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
7176
+ "AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
7177
+ };
7178
+ KEYWORD_INDEX = buildKeywordIndex();
6844
7179
  DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
6845
7180
  TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
6846
7181
  }
6847
7182
  });
6848
7183
 
6849
7184
  // src/lib/tasks-review.ts
6850
- import path13 from "path";
6851
- import { existsSync as existsSync12, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
7185
+ import path14 from "path";
7186
+ import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
6852
7187
  async function countPendingReviews(sessionScope) {
6853
7188
  const client = getClient();
6854
7189
  if (sessionScope) {
@@ -6870,7 +7205,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
6870
7205
  const result2 = await client.execute({
6871
7206
  sql: `SELECT COUNT(*) as cnt FROM tasks
6872
7207
  WHERE status = 'needs_review' AND updated_at > ?
6873
- AND (session_scope = ? OR session_scope IS NULL)`,
7208
+ AND session_scope = ?`,
6874
7209
  args: [sinceIso, sessionScope]
6875
7210
  });
6876
7211
  return Number(result2.rows[0]?.cnt) || 0;
@@ -6888,7 +7223,7 @@ async function listPendingReviews(limit, sessionScope) {
6888
7223
  const result2 = await client.execute({
6889
7224
  sql: `SELECT title, assigned_to, project_name FROM tasks
6890
7225
  WHERE status = 'needs_review'
6891
- AND (session_scope = ? OR session_scope IS NULL)
7226
+ AND session_scope = ?
6892
7227
  ORDER BY priority ASC, created_at DESC LIMIT ?`,
6893
7228
  args: [sessionScope, limit]
6894
7229
  });
@@ -7009,14 +7344,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
7009
7344
  if (parts.length >= 3 && parts[0] === "review") {
7010
7345
  const agent = parts[1];
7011
7346
  const slug = parts.slice(2).join("-");
7012
- const originalTaskFile = `exe/${agent}/${slug}.md`;
7347
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
7013
7348
  const result = await client.execute({
7014
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
7015
- args: [now, originalTaskFile]
7349
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
7350
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
7016
7351
  });
7017
7352
  if (result.rowsAffected > 0) {
7018
7353
  process.stderr.write(
7019
- `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
7354
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
7020
7355
  `
7021
7356
  );
7022
7357
  }
@@ -7029,11 +7364,11 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
7029
7364
  );
7030
7365
  }
7031
7366
  try {
7032
- const cacheDir = path13.join(EXE_AI_DIR, "session-cache");
7033
- if (existsSync12(cacheDir)) {
7367
+ const cacheDir = path14.join(EXE_AI_DIR, "session-cache");
7368
+ if (existsSync13(cacheDir)) {
7034
7369
  for (const f of readdirSync3(cacheDir)) {
7035
7370
  if (f.startsWith("review-notified-")) {
7036
- unlinkSync4(path13.join(cacheDir, f));
7371
+ unlinkSync4(path14.join(cacheDir, f));
7037
7372
  }
7038
7373
  }
7039
7374
  }
@@ -7054,7 +7389,7 @@ var init_tasks_review = __esm({
7054
7389
  });
7055
7390
 
7056
7391
  // src/lib/tasks-chain.ts
7057
- import path14 from "path";
7392
+ import path15 from "path";
7058
7393
  import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
7059
7394
  async function cascadeUnblock(taskId, baseDir, now) {
7060
7395
  const client = getClient();
@@ -7071,7 +7406,7 @@ async function cascadeUnblock(taskId, baseDir, now) {
7071
7406
  });
7072
7407
  for (const ur of unblockedRows.rows) {
7073
7408
  try {
7074
- const ubFile = path14.join(baseDir, String(ur.task_file));
7409
+ const ubFile = path15.join(baseDir, String(ur.task_file));
7075
7410
  let ubContent = await readFile4(ubFile, "utf-8");
7076
7411
  ubContent = ubContent.replace(/\*\*Status:\*\* blocked/, "**Status:** open");
7077
7412
  ubContent = ubContent.replace(/\n\*\*Blocked by:\*\*.*\n/, "\n");
@@ -7140,7 +7475,7 @@ var init_tasks_chain = __esm({
7140
7475
 
7141
7476
  // src/lib/project-name.ts
7142
7477
  import { execSync as execSync5 } from "child_process";
7143
- import path15 from "path";
7478
+ import path16 from "path";
7144
7479
  function getProjectName(cwd) {
7145
7480
  const dir = cwd ?? process.cwd();
7146
7481
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -7153,7 +7488,7 @@ function getProjectName(cwd) {
7153
7488
  timeout: 2e3,
7154
7489
  stdio: ["pipe", "pipe", "pipe"]
7155
7490
  }).trim();
7156
- repoRoot = path15.dirname(gitCommonDir);
7491
+ repoRoot = path16.dirname(gitCommonDir);
7157
7492
  } catch {
7158
7493
  repoRoot = execSync5("git rev-parse --show-toplevel", {
7159
7494
  cwd: dir,
@@ -7162,11 +7497,11 @@ function getProjectName(cwd) {
7162
7497
  stdio: ["pipe", "pipe", "pipe"]
7163
7498
  }).trim();
7164
7499
  }
7165
- _cached2 = path15.basename(repoRoot);
7500
+ _cached2 = path16.basename(repoRoot);
7166
7501
  _cachedCwd = dir;
7167
7502
  return _cached2;
7168
7503
  } catch {
7169
- _cached2 = path15.basename(dir);
7504
+ _cached2 = path16.basename(dir);
7170
7505
  _cachedCwd = dir;
7171
7506
  return _cached2;
7172
7507
  }
@@ -7198,7 +7533,7 @@ function findSessionForProject(projectName) {
7198
7533
  const sessions = listSessions();
7199
7534
  for (const s of sessions) {
7200
7535
  const proj = s.projectDir.split("/").filter(Boolean).pop();
7201
- if (proj === projectName && (s.agentId === "exe" || isCoordinatorName(s.agentId))) return s;
7536
+ if (proj === projectName && isCoordinatorName(s.agentId)) return s;
7202
7537
  }
7203
7538
  return null;
7204
7539
  }
@@ -7244,7 +7579,7 @@ var init_session_scope = __esm({
7244
7579
 
7245
7580
  // src/lib/tasks-notify.ts
7246
7581
  async function dispatchTaskToEmployee(input) {
7247
- if (input.assignedTo === "exe" || isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
7582
+ if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
7248
7583
  let crossProject = false;
7249
7584
  if (input.projectName) {
7250
7585
  try {
@@ -7639,8 +7974,8 @@ __export(tasks_exports, {
7639
7974
  updateTaskStatus: () => updateTaskStatus,
7640
7975
  writeCheckpoint: () => writeCheckpoint
7641
7976
  });
7642
- import path16 from "path";
7643
- import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, unlinkSync as unlinkSync5 } from "fs";
7977
+ import path17 from "path";
7978
+ import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, unlinkSync as unlinkSync5 } from "fs";
7644
7979
  async function createTask(input) {
7645
7980
  const result = await createTaskCore(input);
7646
7981
  if (!input.skipDispatch && result.status !== "blocked" && !process.env.VITEST) {
@@ -7659,11 +7994,11 @@ async function updateTask(input) {
7659
7994
  const { row, taskFile, now, taskId } = await updateTaskStatus(input);
7660
7995
  try {
7661
7996
  const agent = String(row.assigned_to);
7662
- const cacheDir = path16.join(EXE_AI_DIR, "session-cache");
7663
- const cachePath = path16.join(cacheDir, `current-task-${agent}.json`);
7997
+ const cacheDir = path17.join(EXE_AI_DIR, "session-cache");
7998
+ const cachePath = path17.join(cacheDir, `current-task-${agent}.json`);
7664
7999
  if (input.status === "in_progress") {
7665
- mkdirSync6(cacheDir, { recursive: true });
7666
- writeFileSync5(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
8000
+ mkdirSync7(cacheDir, { recursive: true });
8001
+ writeFileSync6(cachePath, JSON.stringify({ taskId, title: String(row.title) }));
7667
8002
  } else if (input.status === "done" || input.status === "blocked" || input.status === "cancelled") {
7668
8003
  try {
7669
8004
  unlinkSync5(cachePath);
@@ -7723,7 +8058,7 @@ async function updateTask(input) {
7723
8058
  }
7724
8059
  const isTerminal = input.status === "done" || input.status === "needs_review";
7725
8060
  if (isTerminal) {
7726
- const isCoordinator = String(row.assigned_to) === "exe" || isCoordinatorName(String(row.assigned_to));
8061
+ const isCoordinator = isCoordinatorName(String(row.assigned_to));
7727
8062
  if (!isCoordinator) {
7728
8063
  notifyTaskDone();
7729
8064
  }
@@ -7748,7 +8083,7 @@ async function updateTask(input) {
7748
8083
  }
7749
8084
  }
7750
8085
  }
7751
- if (input.status === "done" && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
8086
+ if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
7752
8087
  Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
7753
8088
  ({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
7754
8089
  taskId,
@@ -7764,7 +8099,7 @@ async function updateTask(input) {
7764
8099
  });
7765
8100
  }
7766
8101
  let nextTask;
7767
- if (isTerminal && String(row.assigned_to) !== "exe" && !isCoordinatorName(String(row.assigned_to))) {
8102
+ if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
7768
8103
  try {
7769
8104
  nextTask = await findNextTask(String(row.assigned_to));
7770
8105
  } catch {
@@ -8130,13 +8465,13 @@ __export(tmux_routing_exports, {
8130
8465
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
8131
8466
  });
8132
8467
  import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
8133
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync6, mkdirSync as mkdirSync7, existsSync as existsSync13, appendFileSync } from "fs";
8134
- import path17 from "path";
8135
- import os8 from "os";
8468
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8, existsSync as existsSync14, appendFileSync } from "fs";
8469
+ import path18 from "path";
8470
+ import os9 from "os";
8136
8471
  import { fileURLToPath as fileURLToPath2 } from "url";
8137
8472
  import { unlinkSync as unlinkSync6 } from "fs";
8138
8473
  function spawnLockPath(sessionName) {
8139
- return path17.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
8474
+ return path18.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
8140
8475
  }
8141
8476
  function isProcessAlive(pid) {
8142
8477
  try {
@@ -8147,13 +8482,13 @@ function isProcessAlive(pid) {
8147
8482
  }
8148
8483
  }
8149
8484
  function acquireSpawnLock2(sessionName) {
8150
- if (!existsSync13(SPAWN_LOCK_DIR)) {
8151
- mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
8485
+ if (!existsSync14(SPAWN_LOCK_DIR)) {
8486
+ mkdirSync8(SPAWN_LOCK_DIR, { recursive: true });
8152
8487
  }
8153
8488
  const lockFile = spawnLockPath(sessionName);
8154
- if (existsSync13(lockFile)) {
8489
+ if (existsSync14(lockFile)) {
8155
8490
  try {
8156
- const lock = JSON.parse(readFileSync11(lockFile, "utf8"));
8491
+ const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
8157
8492
  const age = Date.now() - lock.timestamp;
8158
8493
  if (isProcessAlive(lock.pid) && age < 6e4) {
8159
8494
  return false;
@@ -8161,7 +8496,7 @@ function acquireSpawnLock2(sessionName) {
8161
8496
  } catch {
8162
8497
  }
8163
8498
  }
8164
- writeFileSync6(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
8499
+ writeFileSync7(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
8165
8500
  return true;
8166
8501
  }
8167
8502
  function releaseSpawnLock2(sessionName) {
@@ -8173,13 +8508,13 @@ function releaseSpawnLock2(sessionName) {
8173
8508
  function resolveBehaviorsExporterScript() {
8174
8509
  try {
8175
8510
  const thisFile = fileURLToPath2(import.meta.url);
8176
- const scriptPath = path17.join(
8177
- path17.dirname(thisFile),
8511
+ const scriptPath = path18.join(
8512
+ path18.dirname(thisFile),
8178
8513
  "..",
8179
8514
  "bin",
8180
8515
  "exe-export-behaviors.js"
8181
8516
  );
8182
- return existsSync13(scriptPath) ? scriptPath : null;
8517
+ return existsSync14(scriptPath) ? scriptPath : null;
8183
8518
  } catch {
8184
8519
  return null;
8185
8520
  }
@@ -8245,12 +8580,12 @@ function extractRootExe(name) {
8245
8580
  return parts.length > 0 ? parts[parts.length - 1] : null;
8246
8581
  }
8247
8582
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
8248
- if (!existsSync13(SESSION_CACHE)) {
8249
- mkdirSync7(SESSION_CACHE, { recursive: true });
8583
+ if (!existsSync14(SESSION_CACHE)) {
8584
+ mkdirSync8(SESSION_CACHE, { recursive: true });
8250
8585
  }
8251
8586
  const rootExe = extractRootExe(parentExe) ?? parentExe;
8252
- const filePath = path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
8253
- writeFileSync6(filePath, JSON.stringify({
8587
+ const filePath = path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
8588
+ writeFileSync7(filePath, JSON.stringify({
8254
8589
  parentExe: rootExe,
8255
8590
  dispatchedBy: dispatchedBy || rootExe,
8256
8591
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -8258,7 +8593,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
8258
8593
  }
8259
8594
  function getParentExe(sessionKey) {
8260
8595
  try {
8261
- const data = JSON.parse(readFileSync11(path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
8596
+ const data = JSON.parse(readFileSync12(path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
8262
8597
  return data.parentExe || null;
8263
8598
  } catch {
8264
8599
  return null;
@@ -8266,8 +8601,8 @@ function getParentExe(sessionKey) {
8266
8601
  }
8267
8602
  function getDispatchedBy(sessionKey) {
8268
8603
  try {
8269
- const data = JSON.parse(readFileSync11(
8270
- path17.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
8604
+ const data = JSON.parse(readFileSync12(
8605
+ path18.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
8271
8606
  "utf8"
8272
8607
  ));
8273
8608
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -8328,32 +8663,50 @@ async function verifyPaneAtCapacity(sessionName) {
8328
8663
  }
8329
8664
  function readDebounceState() {
8330
8665
  try {
8331
- if (!existsSync13(DEBOUNCE_FILE)) return {};
8332
- return JSON.parse(readFileSync11(DEBOUNCE_FILE, "utf8"));
8666
+ if (!existsSync14(DEBOUNCE_FILE)) return {};
8667
+ const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
8668
+ const state = {};
8669
+ for (const [key, val] of Object.entries(raw)) {
8670
+ if (typeof val === "number") {
8671
+ state[key] = { lastSent: val, pending: 0 };
8672
+ } else if (val && typeof val === "object" && "lastSent" in val) {
8673
+ state[key] = val;
8674
+ }
8675
+ }
8676
+ return state;
8333
8677
  } catch {
8334
8678
  return {};
8335
8679
  }
8336
8680
  }
8337
8681
  function writeDebounceState(state) {
8338
8682
  try {
8339
- if (!existsSync13(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
8340
- writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
8683
+ if (!existsSync14(SESSION_CACHE)) mkdirSync8(SESSION_CACHE, { recursive: true });
8684
+ writeFileSync7(DEBOUNCE_FILE, JSON.stringify(state));
8341
8685
  } catch {
8342
8686
  }
8343
8687
  }
8344
8688
  function isDebounced(targetSession) {
8345
8689
  const state = readDebounceState();
8346
- const lastSent = state[targetSession] ?? 0;
8347
- return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
8690
+ const entry = state[targetSession];
8691
+ const lastSent = entry?.lastSent ?? 0;
8692
+ if (Date.now() - lastSent < INTERCOM_DEBOUNCE_MS) {
8693
+ if (!state[targetSession]) state[targetSession] = { lastSent, pending: 0 };
8694
+ state[targetSession].pending++;
8695
+ writeDebounceState(state);
8696
+ return true;
8697
+ }
8698
+ return false;
8348
8699
  }
8349
8700
  function recordDebounce(targetSession) {
8350
8701
  const state = readDebounceState();
8351
- state[targetSession] = Date.now();
8702
+ const batched = state[targetSession]?.pending ?? 0;
8703
+ state[targetSession] = { lastSent: Date.now(), pending: 0 };
8352
8704
  const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
8353
8705
  for (const key of Object.keys(state)) {
8354
- if ((state[key] ?? 0) < cutoff) delete state[key];
8706
+ if ((state[key]?.lastSent ?? 0) < cutoff) delete state[key];
8355
8707
  }
8356
8708
  writeDebounceState(state);
8709
+ return batched;
8357
8710
  }
8358
8711
  function logIntercom(msg) {
8359
8712
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
@@ -8398,7 +8751,7 @@ function sendIntercom(targetSession) {
8398
8751
  return "skipped_exe";
8399
8752
  }
8400
8753
  if (isDebounced(targetSession)) {
8401
- logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
8754
+ logIntercom(`DEBOUNCE \u2192 ${targetSession} (nudge batched, task safe in DB)`);
8402
8755
  return "debounced";
8403
8756
  }
8404
8757
  try {
@@ -8410,14 +8763,14 @@ function sendIntercom(targetSession) {
8410
8763
  const sessionState = getSessionState(targetSession);
8411
8764
  if (sessionState === "no_claude") {
8412
8765
  queueIntercom(targetSession, "claude not running in session");
8413
- recordDebounce(targetSession);
8414
- logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
8766
+ const batched2 = recordDebounce(targetSession);
8767
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
8415
8768
  return "queued";
8416
8769
  }
8417
8770
  if (sessionState === "thinking" || sessionState === "tool") {
8418
8771
  queueIntercom(targetSession, "session busy at send time");
8419
- recordDebounce(targetSession);
8420
- logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
8772
+ const batched2 = recordDebounce(targetSession);
8773
+ logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
8421
8774
  return "queued";
8422
8775
  }
8423
8776
  if (transport.isPaneInCopyMode(targetSession)) {
@@ -8425,8 +8778,8 @@ function sendIntercom(targetSession) {
8425
8778
  transport.sendKeys(targetSession, "q");
8426
8779
  }
8427
8780
  transport.sendKeys(targetSession, "/exe-intercom");
8428
- recordDebounce(targetSession);
8429
- logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
8781
+ const batched = recordDebounce(targetSession);
8782
+ logIntercom(`DELIVERED \u2192 ${targetSession}${batched > 0 ? ` [${batched} nudges batched during debounce]` : ""} (fire-and-forget)`);
8430
8783
  return "delivered";
8431
8784
  } catch {
8432
8785
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -8456,7 +8809,7 @@ function notifyParentExe(sessionKey) {
8456
8809
  return true;
8457
8810
  }
8458
8811
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
8459
- if (employeeName === "exe" || isCoordinatorName(employeeName)) {
8812
+ if (isCoordinatorName(employeeName)) {
8460
8813
  return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
8461
8814
  }
8462
8815
  try {
@@ -8528,26 +8881,26 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8528
8881
  const transport = getTransport();
8529
8882
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
8530
8883
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
8531
- const logDir = path17.join(os8.homedir(), ".exe-os", "session-logs");
8532
- const logFile = path17.join(logDir, `${instanceLabel}-${Date.now()}.log`);
8533
- if (!existsSync13(logDir)) {
8534
- mkdirSync7(logDir, { recursive: true });
8884
+ const logDir = path18.join(os9.homedir(), ".exe-os", "session-logs");
8885
+ const logFile = path18.join(logDir, `${instanceLabel}-${Date.now()}.log`);
8886
+ if (!existsSync14(logDir)) {
8887
+ mkdirSync8(logDir, { recursive: true });
8535
8888
  }
8536
8889
  transport.kill(sessionName);
8537
8890
  let cleanupSuffix = "";
8538
8891
  try {
8539
8892
  const thisFile = fileURLToPath2(import.meta.url);
8540
- const cleanupScript = path17.join(path17.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
8541
- if (existsSync13(cleanupScript)) {
8893
+ const cleanupScript = path18.join(path18.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
8894
+ if (existsSync14(cleanupScript)) {
8542
8895
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
8543
8896
  }
8544
8897
  } catch {
8545
8898
  }
8546
8899
  try {
8547
- const claudeJsonPath = path17.join(os8.homedir(), ".claude.json");
8900
+ const claudeJsonPath = path18.join(os9.homedir(), ".claude.json");
8548
8901
  let claudeJson = {};
8549
8902
  try {
8550
- claudeJson = JSON.parse(readFileSync11(claudeJsonPath, "utf8"));
8903
+ claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
8551
8904
  } catch {
8552
8905
  }
8553
8906
  if (!claudeJson.projects) claudeJson.projects = {};
@@ -8555,17 +8908,17 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8555
8908
  const trustDir = opts?.cwd ?? projectDir;
8556
8909
  if (!projects[trustDir]) projects[trustDir] = {};
8557
8910
  projects[trustDir].hasTrustDialogAccepted = true;
8558
- writeFileSync6(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
8911
+ writeFileSync7(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
8559
8912
  } catch {
8560
8913
  }
8561
8914
  try {
8562
- const settingsDir = path17.join(os8.homedir(), ".claude", "projects");
8915
+ const settingsDir = path18.join(os9.homedir(), ".claude", "projects");
8563
8916
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
8564
- const projSettingsDir = path17.join(settingsDir, normalizedKey);
8565
- const settingsPath = path17.join(projSettingsDir, "settings.json");
8917
+ const projSettingsDir = path18.join(settingsDir, normalizedKey);
8918
+ const settingsPath = path18.join(projSettingsDir, "settings.json");
8566
8919
  let settings = {};
8567
8920
  try {
8568
- settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
8921
+ settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
8569
8922
  } catch {
8570
8923
  }
8571
8924
  const perms = settings.permissions ?? {};
@@ -8593,21 +8946,24 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8593
8946
  if (changed) {
8594
8947
  perms.allow = allow;
8595
8948
  settings.permissions = perms;
8596
- mkdirSync7(projSettingsDir, { recursive: true });
8597
- writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + "\n");
8949
+ mkdirSync8(projSettingsDir, { recursive: true });
8950
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + "\n");
8598
8951
  }
8599
8952
  } catch {
8600
8953
  }
8601
8954
  const spawnCwd = opts?.cwd ?? projectDir;
8602
8955
  const useExeAgent = !!(opts?.model && opts?.provider);
8603
- const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
8956
+ const agentRtConfig = getAgentRuntime(employeeName);
8957
+ const useCodex = !useExeAgent && agentRtConfig.runtime === "codex";
8958
+ const useOpencode = !useExeAgent && !useCodex && agentRtConfig.runtime === "opencode";
8959
+ const ccProvider = useExeAgent || useCodex || useOpencode ? DEFAULT_PROVIDER : detectActiveProvider();
8604
8960
  const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
8605
8961
  let identityFlag = "";
8606
8962
  let behaviorsFlag = "";
8607
8963
  let legacyFallbackWarned = false;
8608
8964
  if (!useExeAgent && !useBinSymlink) {
8609
- const identityPath = path17.join(
8610
- os8.homedir(),
8965
+ const identityPath = path18.join(
8966
+ os9.homedir(),
8611
8967
  ".exe-os",
8612
8968
  "identity",
8613
8969
  `${employeeName}.md`
@@ -8616,13 +8972,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8616
8972
  const hasAgentFlag = claudeSupportsAgentFlag();
8617
8973
  if (hasAgentFlag) {
8618
8974
  identityFlag = ` --agent ${employeeName}`;
8619
- } else if (existsSync13(identityPath)) {
8975
+ } else if (existsSync14(identityPath)) {
8620
8976
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
8621
8977
  legacyFallbackWarned = true;
8622
8978
  }
8623
8979
  const behaviorsFile = exportBehaviorsSync(
8624
8980
  employeeName,
8625
- path17.basename(spawnCwd),
8981
+ path18.basename(spawnCwd),
8626
8982
  sessionName
8627
8983
  );
8628
8984
  if (behaviorsFile) {
@@ -8637,16 +8993,16 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8637
8993
  }
8638
8994
  let sessionContextFlag = "";
8639
8995
  try {
8640
- const ctxDir = path17.join(os8.homedir(), ".exe-os", "session-cache");
8641
- mkdirSync7(ctxDir, { recursive: true });
8642
- const ctxFile = path17.join(ctxDir, `session-context-${sessionName}.md`);
8996
+ const ctxDir = path18.join(os9.homedir(), ".exe-os", "session-cache");
8997
+ mkdirSync8(ctxDir, { recursive: true });
8998
+ const ctxFile = path18.join(ctxDir, `session-context-${sessionName}.md`);
8643
8999
  const ctxContent = [
8644
9000
  `## Session Context`,
8645
9001
  `You are running in tmux session: ${sessionName}.`,
8646
9002
  `Your parent coordinator session is ${exeSession}.`,
8647
9003
  `Your employees (if any) use the -${exeSession} suffix.`
8648
9004
  ].join("\n");
8649
- writeFileSync6(ctxFile, ctxContent);
9005
+ writeFileSync7(ctxFile, ctxContent);
8650
9006
  sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
8651
9007
  } catch {
8652
9008
  }
@@ -8660,9 +9016,48 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8660
9016
  }
8661
9017
  }
8662
9018
  }
9019
+ if (useCodex) {
9020
+ const codexCfg = RUNTIME_TABLE.codex;
9021
+ if (codexCfg?.apiKeyEnv) {
9022
+ const keyVal = process.env[codexCfg.apiKeyEnv];
9023
+ if (keyVal) {
9024
+ envPrefix = `${envPrefix} ${codexCfg.apiKeyEnv}=${keyVal}`;
9025
+ }
9026
+ }
9027
+ envPrefix = `${envPrefix} EXE_AGENT_MODEL=${agentRtConfig.model}`;
9028
+ }
9029
+ if (useOpencode) {
9030
+ const ocCfg = PROVIDER_TABLE.opencode;
9031
+ if (ocCfg?.apiKeyEnv) {
9032
+ const keyVal = process.env[ocCfg.apiKeyEnv];
9033
+ if (keyVal) {
9034
+ envPrefix = `${envPrefix} ${ocCfg.apiKeyEnv}=${keyVal}`;
9035
+ }
9036
+ }
9037
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
9038
+ }
9039
+ if (!useExeAgent && !useCodex && !useOpencode && !useBinSymlink) {
9040
+ const defaultClaudeModel = DEFAULT_MODELS.claude;
9041
+ if (agentRtConfig.runtime === "claude" && agentRtConfig.model !== defaultClaudeModel) {
9042
+ envPrefix = `${envPrefix} ANTHROPIC_MODEL=${agentRtConfig.model}`;
9043
+ }
9044
+ }
8663
9045
  let spawnCommand;
8664
9046
  if (useExeAgent) {
8665
9047
  spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
9048
+ } else if (useCodex) {
9049
+ process.stderr.write(
9050
+ `[tmux-routing] agent-config: ${employeeName} \u2192 codex (${agentRtConfig.model})
9051
+ `
9052
+ );
9053
+ spawnCommand = `${envPrefix} exe-start-codex --agent ${employeeName}${cleanupSuffix}`;
9054
+ } else if (useOpencode) {
9055
+ const binName = `${employeeName}-opencode`;
9056
+ process.stderr.write(
9057
+ `[tmux-routing] agent-config: ${employeeName} \u2192 opencode (${agentRtConfig.model})
9058
+ `
9059
+ );
9060
+ spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
8666
9061
  } else if (useBinSymlink) {
8667
9062
  const binName = `${employeeName}-${ccProvider}`;
8668
9063
  process.stderr.write(
@@ -8684,11 +9079,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8684
9079
  transport.pipeLog(sessionName, logFile);
8685
9080
  try {
8686
9081
  const mySession = getMySession();
8687
- const dispatchInfo = path17.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
8688
- writeFileSync6(dispatchInfo, JSON.stringify({
9082
+ const dispatchInfo = path18.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
9083
+ writeFileSync7(dispatchInfo, JSON.stringify({
8689
9084
  dispatchedBy: mySession,
8690
9085
  rootExe: exeSession,
8691
- provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
9086
+ provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : useCodex ? "openai" : useOpencode ? "opencode" : "anthropic",
9087
+ runtime: useCodex ? "codex" : useOpencode ? "opencode" : useExeAgent ? "exe-agent" : "claude",
9088
+ model: useCodex ? agentRtConfig.model : useOpencode ? agentRtConfig.model : void 0,
8692
9089
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
8693
9090
  }));
8694
9091
  } catch {
@@ -8706,6 +9103,11 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8706
9103
  booted = true;
8707
9104
  break;
8708
9105
  }
9106
+ } else if (useCodex) {
9107
+ if (pane.includes("codex") || pane.includes("Codex") || pane.includes("exe-start-codex")) {
9108
+ booted = true;
9109
+ break;
9110
+ }
8709
9111
  } else {
8710
9112
  if (pane.includes("Claude Code") || pane.includes("\u276F")) {
8711
9113
  booted = true;
@@ -8717,9 +9119,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8717
9119
  }
8718
9120
  if (!booted) {
8719
9121
  releaseSpawnLock2(sessionName);
8720
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
9122
+ const runtimeLabel = useExeAgent ? "exe-agent" : useCodex ? "codex" : "claude";
9123
+ return { sessionName, error: `${runtimeLabel} did not boot within 15s` };
8721
9124
  }
8722
- if (!useExeAgent) {
9125
+ if (!useExeAgent && !useCodex) {
8723
9126
  try {
8724
9127
  transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
8725
9128
  } catch {
@@ -8746,17 +9149,19 @@ var init_tmux_routing = __esm({
8746
9149
  init_cc_agent_support();
8747
9150
  init_mcp_prefix();
8748
9151
  init_provider_table();
9152
+ init_agent_config();
9153
+ init_runtime_table();
8749
9154
  init_intercom_queue();
8750
9155
  init_plan_limits();
8751
9156
  init_employees();
8752
- SPAWN_LOCK_DIR = path17.join(os8.homedir(), ".exe-os", "spawn-locks");
8753
- SESSION_CACHE = path17.join(os8.homedir(), ".exe-os", "session-cache");
9157
+ SPAWN_LOCK_DIR = path18.join(os9.homedir(), ".exe-os", "spawn-locks");
9158
+ SESSION_CACHE = path18.join(os9.homedir(), ".exe-os", "session-cache");
8754
9159
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
8755
9160
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8756
9161
  VERIFY_PANE_LINES = 200;
8757
9162
  INTERCOM_DEBOUNCE_MS = 3e4;
8758
- INTERCOM_LOG2 = path17.join(os8.homedir(), ".exe-os", "intercom.log");
8759
- DEBOUNCE_FILE = path17.join(SESSION_CACHE, "intercom-debounce.json");
9163
+ INTERCOM_LOG2 = path18.join(os9.homedir(), ".exe-os", "intercom.log");
9164
+ DEBOUNCE_FILE = path18.join(SESSION_CACHE, "intercom-debounce.json");
8760
9165
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
8761
9166
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
8762
9167
  }
@@ -9014,10 +9419,10 @@ var init_messaging = __esm({
9014
9419
  });
9015
9420
 
9016
9421
  // src/automation/trigger-engine.ts
9017
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
9422
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync8, existsSync as existsSync15, mkdirSync as mkdirSync9 } from "fs";
9018
9423
  import { randomUUID as randomUUID8 } from "crypto";
9019
- import path18 from "path";
9020
- import os9 from "os";
9424
+ import path19 from "path";
9425
+ import os10 from "os";
9021
9426
  function substituteTemplate(template, record) {
9022
9427
  return template.replace(
9023
9428
  /\{\{(\w+(?:\.\w+)*)\}\}/g,
@@ -9070,9 +9475,9 @@ function evaluateConditions(conditions, record) {
9070
9475
  return conditions.every((c) => evaluateCondition(c, record));
9071
9476
  }
9072
9477
  function loadTriggers(project) {
9073
- if (!existsSync14(TRIGGERS_PATH)) return [];
9478
+ if (!existsSync15(TRIGGERS_PATH)) return [];
9074
9479
  try {
9075
- const raw = readFileSync12(TRIGGERS_PATH, "utf-8");
9480
+ const raw = readFileSync13(TRIGGERS_PATH, "utf-8");
9076
9481
  const all = JSON.parse(raw);
9077
9482
  if (!Array.isArray(all)) return [];
9078
9483
  if (project) {
@@ -9312,7 +9717,7 @@ var TRIGGERS_PATH, GRAPH_API_VERSION;
9312
9717
  var init_trigger_engine = __esm({
9313
9718
  "src/automation/trigger-engine.ts"() {
9314
9719
  "use strict";
9315
- TRIGGERS_PATH = path18.join(os9.homedir(), ".exe-os", "triggers.json");
9720
+ TRIGGERS_PATH = path19.join(os10.homedir(), ".exe-os", "triggers.json");
9316
9721
  GRAPH_API_VERSION = "v21.0";
9317
9722
  }
9318
9723
  });
@@ -9375,15 +9780,15 @@ var init_crm_webhook = __esm({
9375
9780
  });
9376
9781
 
9377
9782
  // src/bin/exe-gateway.ts
9378
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
9379
- import path19 from "path";
9380
- import os10 from "os";
9783
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
9784
+ import path20 from "path";
9785
+ import os11 from "os";
9381
9786
 
9382
9787
  // src/gateway/webhook-server.ts
9383
9788
  import {
9384
9789
  createServer
9385
9790
  } from "http";
9386
- var DEFAULT_HOST = "127.0.0.1";
9791
+ var DEFAULT_HOST = process.env.EXE_WEBHOOK_HOST || "127.0.0.1";
9387
9792
  var BODY_SIZE_LIMIT = 1048576;
9388
9793
  var WebhookServer = class {
9389
9794
  constructor(config2) {
@@ -10069,10 +10474,49 @@ function buildPermissionContext(platform, permissions) {
10069
10474
  return `[${platform.toUpperCase()} \u2014 allowed: ${parts.join(", ")}]`;
10070
10475
  }
10071
10476
 
10477
+ // src/gateway/bot-errors.ts
10478
+ var FatalBotError = class extends Error {
10479
+ constructor(message, cause) {
10480
+ super(message);
10481
+ this.cause = cause;
10482
+ this.name = "FatalBotError";
10483
+ }
10484
+ fatal = true;
10485
+ };
10486
+ var RecoverableBotError = class extends Error {
10487
+ constructor(message, toolName, cause) {
10488
+ super(message);
10489
+ this.toolName = toolName;
10490
+ this.cause = cause;
10491
+ this.name = "RecoverableBotError";
10492
+ }
10493
+ recoverable = true;
10494
+ };
10495
+ var MaxStepsError = class extends Error {
10496
+ constructor(stepsTaken, maxSteps) {
10497
+ super(
10498
+ `Reached maximum steps (${stepsTaken}/${maxSteps}). Returning partial result.`
10499
+ );
10500
+ this.stepsTaken = stepsTaken;
10501
+ this.maxSteps = maxSteps;
10502
+ this.name = "MaxStepsError";
10503
+ }
10504
+ };
10505
+ function classifyError(err, toolName) {
10506
+ if (err instanceof FatalBotError) return err;
10507
+ if (err instanceof RecoverableBotError) return err;
10508
+ const message = err instanceof Error ? err.message : String(err);
10509
+ if (message.includes("401") || message.includes("403") || message.includes("authentication") || message.includes("rate_limit")) {
10510
+ return new FatalBotError(message, err);
10511
+ }
10512
+ return new RecoverableBotError(message, toolName, err);
10513
+ }
10514
+
10072
10515
  // src/gateway/bot-runtime.ts
10073
10516
  var DEFAULT_MODEL = "claude-sonnet-4-20250514";
10074
10517
  var MAX_TURNS = 10;
10075
10518
  var MAX_HISTORY = 50;
10519
+ var DEFAULT_PLANNING_INTERVAL = 3;
10076
10520
  function filterToolsForPermissions(tools, permissions) {
10077
10521
  const allAllowed = /* @__PURE__ */ new Set();
10078
10522
  if (permissions.canRead) {
@@ -10101,7 +10545,7 @@ var BotRuntime = class {
10101
10545
  async processMessage(msg, permissions) {
10102
10546
  const sessionKey = msg.chatType === "group" ? msg.channelId : msg.senderId;
10103
10547
  const history = this.getHistory(sessionKey);
10104
- history.push({ role: "user", content: msg.text });
10548
+ history.push({ role: "user", content: msg.text, frameType: "task" });
10105
10549
  const systemPrompt = this.config.systemPrompt + "\n\n" + buildPermissionContext(msg.platform, permissions);
10106
10550
  const allowedTools = filterToolsForPermissions(
10107
10551
  this.config.tools,
@@ -10109,29 +10553,54 @@ var BotRuntime = class {
10109
10553
  );
10110
10554
  const model = this.config.model ?? DEFAULT_MODEL;
10111
10555
  const maxTurns = this.config.maxTurns ?? MAX_TURNS;
10556
+ const planningInterval = this.config.planningInterval ?? DEFAULT_PLANNING_INTERVAL;
10112
10557
  let turns = 0;
10113
10558
  while (turns < maxTurns) {
10114
10559
  turns++;
10115
- const response = await this.client.messages.create({
10116
- model,
10117
- max_tokens: 4096,
10118
- system: systemPrompt,
10119
- messages: history.map((m) => ({
10120
- role: m.role,
10121
- content: m.content
10122
- })),
10123
- tools: allowedTools.map((t) => ({
10124
- name: t.name,
10125
- description: t.description,
10126
- input_schema: t.input_schema
10127
- }))
10128
- });
10560
+ if (planningInterval > 0 && turns > 1 && turns % planningInterval === 1 && maxTurns - turns >= 2) {
10561
+ history.push({
10562
+ role: "user",
10563
+ content: "[Planning checkpoint] Review what you know so far. What facts have you gathered? What is your plan for the remaining steps? Be concise.",
10564
+ frameType: "planning"
10565
+ });
10566
+ }
10567
+ let response;
10568
+ try {
10569
+ response = await this.client.messages.create({
10570
+ model,
10571
+ max_tokens: 4096,
10572
+ system: systemPrompt,
10573
+ messages: history.map((m) => ({
10574
+ role: m.role,
10575
+ content: m.content
10576
+ })),
10577
+ tools: allowedTools.map((t) => ({
10578
+ name: t.name,
10579
+ description: t.description,
10580
+ input_schema: t.input_schema
10581
+ }))
10582
+ });
10583
+ } catch (err) {
10584
+ const classified = classifyError(err);
10585
+ if (classified instanceof FatalBotError) {
10586
+ const errorMsg = `Bot error: ${classified.message}`;
10587
+ history.push({ role: "assistant", content: errorMsg, frameType: "error" });
10588
+ this.trimHistory(sessionKey);
10589
+ return errorMsg;
10590
+ }
10591
+ history.push({
10592
+ role: "assistant",
10593
+ content: `[API error \u2014 retrying] ${classified.message}`,
10594
+ frameType: "error"
10595
+ });
10596
+ continue;
10597
+ }
10129
10598
  const toolUseBlocks = response.content.filter(
10130
10599
  (b) => b.type === "tool_use"
10131
10600
  );
10132
10601
  if (toolUseBlocks.length === 0) {
10133
10602
  const textContent = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
10134
- history.push({ role: "assistant", content: textContent });
10603
+ history.push({ role: "assistant", content: textContent, frameType: "assistant" });
10135
10604
  this.trimHistory(sessionKey);
10136
10605
  return textContent;
10137
10606
  }
@@ -10141,9 +10610,11 @@ var BotRuntime = class {
10141
10610
  );
10142
10611
  history.push({
10143
10612
  role: "assistant",
10144
- content: response.content
10613
+ content: response.content,
10614
+ frameType: "assistant"
10145
10615
  });
10146
10616
  const toolResults = [];
10617
+ let hadFatalError = false;
10147
10618
  for (const block of allowed) {
10148
10619
  try {
10149
10620
  const result = await this.config.toolExecutor(
@@ -10156,12 +10627,23 @@ var BotRuntime = class {
10156
10627
  content: result
10157
10628
  });
10158
10629
  } catch (err) {
10159
- toolResults.push({
10160
- type: "tool_result",
10161
- tool_use_id: block.id,
10162
- content: `Error: ${err instanceof Error ? err.message : String(err)}`,
10163
- is_error: true
10164
- });
10630
+ const classified = classifyError(err, block.name);
10631
+ if (classified instanceof FatalBotError) {
10632
+ toolResults.push({
10633
+ type: "tool_result",
10634
+ tool_use_id: block.id,
10635
+ content: `Fatal error: ${classified.message}`,
10636
+ is_error: true
10637
+ });
10638
+ hadFatalError = true;
10639
+ } else {
10640
+ toolResults.push({
10641
+ type: "tool_result",
10642
+ tool_use_id: block.id,
10643
+ content: `Error (recoverable): ${classified.message}`,
10644
+ is_error: true
10645
+ });
10646
+ }
10165
10647
  }
10166
10648
  }
10167
10649
  for (const { block, check } of blocked) {
@@ -10174,10 +10656,22 @@ var BotRuntime = class {
10174
10656
  }
10175
10657
  history.push({
10176
10658
  role: "user",
10177
- content: toolResults
10659
+ content: toolResults,
10660
+ frameType: hadFatalError ? "error" : "tool_result"
10178
10661
  });
10662
+ if (hadFatalError) {
10663
+ this.trimHistory(sessionKey);
10664
+ return `A fatal error occurred during tool execution. The bot loop has been stopped.`;
10665
+ }
10179
10666
  }
10180
- return "I reached the maximum number of tool calls for this request. Please try again with a more specific question.";
10667
+ const maxErr = new MaxStepsError(turns, maxTurns);
10668
+ history.push({
10669
+ role: "assistant",
10670
+ content: maxErr.message,
10671
+ frameType: "error"
10672
+ });
10673
+ this.trimHistory(sessionKey);
10674
+ return maxErr.message;
10181
10675
  }
10182
10676
  getHistory(sessionKey) {
10183
10677
  if (!this.conversations.has(sessionKey)) {
@@ -10187,9 +10681,19 @@ var BotRuntime = class {
10187
10681
  }
10188
10682
  trimHistory(sessionKey) {
10189
10683
  const history = this.conversations.get(sessionKey);
10190
- if (history && history.length > MAX_HISTORY) {
10191
- this.conversations.set(sessionKey, history.slice(-MAX_HISTORY));
10684
+ if (!history || history.length <= MAX_HISTORY) return;
10685
+ const firstTaskIdx = history.findIndex((m) => m.frameType === "task");
10686
+ let trimmed = history.filter(
10687
+ (m, i) => m.frameType !== "planning" || i >= history.length - MAX_HISTORY
10688
+ );
10689
+ if (trimmed.length > MAX_HISTORY) {
10690
+ const tail = trimmed.slice(-MAX_HISTORY);
10691
+ if (firstTaskIdx >= 0 && !tail.includes(history[firstTaskIdx])) {
10692
+ tail[0] = history[firstTaskIdx];
10693
+ }
10694
+ trimmed = tail;
10192
10695
  }
10696
+ this.conversations.set(sessionKey, trimmed);
10193
10697
  }
10194
10698
  /** Clear conversation history for a session */
10195
10699
  clearHistory(sessionKey) {
@@ -10244,18 +10748,18 @@ var BotRegistry = class {
10244
10748
 
10245
10749
  // src/bin/exe-gateway.ts
10246
10750
  init_employees();
10247
- var CONFIG_DIR = path19.join(os10.homedir(), ".exe-os");
10248
- var CONFIG_PATH3 = path19.join(CONFIG_DIR, "gateway.json");
10751
+ var CONFIG_DIR = path20.join(os11.homedir(), ".exe-os");
10752
+ var CONFIG_PATH3 = path20.join(CONFIG_DIR, "gateway.json");
10249
10753
  var DEFAULT_PORT = 3100;
10250
10754
  function loadConfig2() {
10251
- if (!existsSync15(CONFIG_PATH3)) {
10755
+ if (!existsSync16(CONFIG_PATH3)) {
10252
10756
  console.log(
10253
10757
  `[exe-gateway] No config at ${CONFIG_PATH3} \u2014 using defaults (port ${DEFAULT_PORT})`
10254
10758
  );
10255
10759
  return {};
10256
10760
  }
10257
10761
  try {
10258
- const raw = readFileSync13(CONFIG_PATH3, "utf-8");
10762
+ const raw = readFileSync14(CONFIG_PATH3, "utf-8");
10259
10763
  return JSON.parse(raw);
10260
10764
  } catch (err) {
10261
10765
  console.error(