@askexenow/exe-os 0.8.0 → 0.8.1

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 (90) hide show
  1. package/README.md +178 -79
  2. package/dist/bin/backfill-responses.js +160 -8
  3. package/dist/bin/backfill-vectors.js +130 -1
  4. package/dist/bin/cleanup-stale-review-tasks.js +130 -1
  5. package/dist/bin/cli.js +10111 -7540
  6. package/dist/bin/exe-agent.js +159 -1
  7. package/dist/bin/exe-assign.js +235 -16
  8. package/dist/bin/exe-boot.js +344 -472
  9. package/dist/bin/exe-call.js +145 -1
  10. package/dist/bin/exe-cloud.js +11 -0
  11. package/dist/bin/exe-dispatch.js +37 -24
  12. package/dist/bin/exe-doctor.js +130 -1
  13. package/dist/bin/exe-export-behaviors.js +150 -7
  14. package/dist/bin/exe-forget.js +822 -665
  15. package/dist/bin/exe-gateway.js +470 -62
  16. package/dist/bin/exe-heartbeat.js +133 -2
  17. package/dist/bin/exe-kill.js +150 -7
  18. package/dist/bin/exe-launch-agent.js +150 -7
  19. package/dist/bin/exe-new-employee.js +756 -224
  20. package/dist/bin/exe-pending-messages.js +132 -2
  21. package/dist/bin/exe-pending-notifications.js +130 -1
  22. package/dist/bin/exe-pending-reviews.js +132 -2
  23. package/dist/bin/exe-review.js +160 -8
  24. package/dist/bin/exe-search.js +2473 -2008
  25. package/dist/bin/exe-session-cleanup.js +238 -51
  26. package/dist/bin/exe-settings.js +11 -0
  27. package/dist/bin/exe-status.js +130 -1
  28. package/dist/bin/exe-team.js +130 -1
  29. package/dist/bin/git-sweep.js +272 -16
  30. package/dist/bin/graph-backfill.js +150 -7
  31. package/dist/bin/graph-export.js +150 -7
  32. package/dist/bin/install.js +5 -0
  33. package/dist/bin/scan-tasks.js +238 -19
  34. package/dist/bin/setup.js +1776 -10
  35. package/dist/bin/shard-migrate.js +150 -7
  36. package/dist/bin/update.js +9 -6
  37. package/dist/bin/wiki-sync.js +150 -7
  38. package/dist/gateway/index.js +470 -62
  39. package/dist/hooks/bug-report-worker.js +195 -35
  40. package/dist/hooks/commit-complete.js +272 -16
  41. package/dist/hooks/error-recall.js +2313 -1847
  42. package/dist/hooks/exe-heartbeat-hook.js +5 -0
  43. package/dist/hooks/ingest-worker.js +330 -58
  44. package/dist/hooks/ingest.js +11 -0
  45. package/dist/hooks/instructions-loaded.js +199 -10
  46. package/dist/hooks/notification.js +199 -10
  47. package/dist/hooks/post-compact.js +199 -10
  48. package/dist/hooks/pre-compact.js +199 -10
  49. package/dist/hooks/pre-tool-use.js +199 -10
  50. package/dist/hooks/prompt-ingest-worker.js +179 -14
  51. package/dist/hooks/prompt-submit.js +781 -285
  52. package/dist/hooks/response-ingest-worker.js +1900 -1405
  53. package/dist/hooks/session-end.js +456 -12
  54. package/dist/hooks/session-start.js +2188 -1724
  55. package/dist/hooks/stop.js +200 -10
  56. package/dist/hooks/subagent-stop.js +199 -10
  57. package/dist/hooks/summary-worker.js +604 -334
  58. package/dist/index.js +554 -61
  59. package/dist/lib/cloud-sync.js +5 -0
  60. package/dist/lib/config.js +13 -0
  61. package/dist/lib/consolidation.js +5 -0
  62. package/dist/lib/database.js +104 -0
  63. package/dist/lib/device-registry.js +109 -0
  64. package/dist/lib/embedder.js +13 -0
  65. package/dist/lib/employee-templates.js +53 -26
  66. package/dist/lib/employees.js +5 -0
  67. package/dist/lib/exe-daemon-client.js +5 -0
  68. package/dist/lib/exe-daemon.js +493 -79
  69. package/dist/lib/file-grep.js +20 -4
  70. package/dist/lib/hybrid-search.js +1435 -190
  71. package/dist/lib/identity-templates.js +126 -5
  72. package/dist/lib/identity.js +5 -0
  73. package/dist/lib/license.js +5 -0
  74. package/dist/lib/messaging.js +37 -24
  75. package/dist/lib/schedules.js +130 -1
  76. package/dist/lib/skill-learning.js +11 -0
  77. package/dist/lib/status-brief.js +5 -0
  78. package/dist/lib/store.js +199 -10
  79. package/dist/lib/task-router.js +72 -6
  80. package/dist/lib/tasks.js +179 -50
  81. package/dist/lib/tmux-routing.js +179 -46
  82. package/dist/mcp/server.js +2129 -1855
  83. package/dist/mcp/tools/create-task.js +86 -36
  84. package/dist/mcp/tools/deactivate-behavior.js +5 -0
  85. package/dist/mcp/tools/list-tasks.js +39 -11
  86. package/dist/mcp/tools/send-message.js +37 -24
  87. package/dist/mcp/tools/update-task.js +153 -38
  88. package/dist/runtime/index.js +451 -59
  89. package/dist/tui/App.js +454 -59
  90. package/package.json +1 -1
@@ -488,6 +488,27 @@ async function ensureSchema() {
488
488
  });
489
489
  } catch {
490
490
  }
491
+ try {
492
+ await client.execute({
493
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint TEXT`,
494
+ args: []
495
+ });
496
+ } catch {
497
+ }
498
+ try {
499
+ await client.execute({
500
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER NOT NULL DEFAULT 0`,
501
+ args: []
502
+ });
503
+ } catch {
504
+ }
505
+ try {
506
+ await client.execute({
507
+ sql: `ALTER TABLE tasks ADD COLUMN complexity TEXT NOT NULL DEFAULT 'standard'`,
508
+ args: []
509
+ });
510
+ } catch {
511
+ }
491
512
  try {
492
513
  await client.execute({
493
514
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -898,6 +919,15 @@ async function ensureSchema() {
898
919
  } catch {
899
920
  }
900
921
  }
922
+ for (const col of [
923
+ "ALTER TABLE memories ADD COLUMN source_path TEXT",
924
+ "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'"
925
+ ]) {
926
+ try {
927
+ await client.execute(col);
928
+ } catch {
929
+ }
930
+ }
901
931
  await client.executeMultiple(`
902
932
  CREATE INDEX IF NOT EXISTS idx_memories_workspace
903
933
  ON memories(workspace_id);
@@ -962,6 +992,34 @@ async function ensureSchema() {
962
992
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
963
993
  ON conversations(channel_id);
964
994
  `);
995
+ try {
996
+ await client.execute({
997
+ sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
998
+ args: []
999
+ });
1000
+ } catch {
1001
+ }
1002
+ try {
1003
+ await client.execute({
1004
+ sql: `ALTER TABLE tasks ADD COLUMN budget_fallback_model TEXT`,
1005
+ args: []
1006
+ });
1007
+ } catch {
1008
+ }
1009
+ try {
1010
+ await client.execute({
1011
+ sql: `ALTER TABLE tasks ADD COLUMN tokens_used INTEGER DEFAULT 0`,
1012
+ args: []
1013
+ });
1014
+ } catch {
1015
+ }
1016
+ try {
1017
+ await client.execute({
1018
+ sql: `ALTER TABLE tasks ADD COLUMN tokens_warned_at INTEGER`,
1019
+ args: []
1020
+ });
1021
+ } catch {
1022
+ }
965
1023
  await client.executeMultiple(`
966
1024
  CREATE VIRTUAL TABLE IF NOT EXISTS conversations_fts USING fts5(
967
1025
  content_text,
@@ -988,6 +1046,52 @@ async function ensureSchema() {
988
1046
  VALUES (new.rowid, new.content_text, new.sender_name, new.agent_response);
989
1047
  END;
990
1048
  `);
1049
+ try {
1050
+ await client.execute({
1051
+ sql: `ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3`,
1052
+ args: []
1053
+ });
1054
+ } catch {
1055
+ }
1056
+ try {
1057
+ await client.execute(
1058
+ `CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)`
1059
+ );
1060
+ } catch {
1061
+ }
1062
+ try {
1063
+ await client.execute({
1064
+ sql: `UPDATE memories SET tier = 1 WHERE tool_name = 'commit_to_long_term_memory' AND importance >= 8 AND tier = 3`,
1065
+ args: []
1066
+ });
1067
+ await client.execute({
1068
+ sql: `UPDATE memories SET tier = 2 WHERE tool_name IN ('store_memory', 'manual') AND importance >= 5 AND tier = 3`,
1069
+ args: []
1070
+ });
1071
+ } catch {
1072
+ }
1073
+ try {
1074
+ await client.execute({
1075
+ sql: `ALTER TABLE memories ADD COLUMN supersedes_id TEXT`,
1076
+ args: []
1077
+ });
1078
+ } catch {
1079
+ }
1080
+ try {
1081
+ await client.execute(
1082
+ `CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL`
1083
+ );
1084
+ } catch {
1085
+ }
1086
+ for (const col of [
1087
+ "ALTER TABLE tasks ADD COLUMN checkpoint TEXT",
1088
+ "ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER DEFAULT 0"
1089
+ ]) {
1090
+ try {
1091
+ await client.execute(col);
1092
+ } catch {
1093
+ }
1094
+ }
991
1095
  }
992
1096
  async function disposeDatabase() {
993
1097
  if (_client) {
@@ -1095,6 +1199,11 @@ function normalizeSessionLifecycle(raw) {
1095
1199
  const userSL = raw.sessionLifecycle ?? {};
1096
1200
  raw.sessionLifecycle = { ...defaultSL, ...userSL };
1097
1201
  }
1202
+ function normalizeAutoUpdate(raw) {
1203
+ const defaultAU = DEFAULT_CONFIG.autoUpdate;
1204
+ const userAU = raw.autoUpdate ?? {};
1205
+ raw.autoUpdate = { ...defaultAU, ...userAU };
1206
+ }
1098
1207
  async function loadConfig() {
1099
1208
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1100
1209
  await mkdir(dir, { recursive: true });
@@ -1117,6 +1226,7 @@ async function loadConfig() {
1117
1226
  }
1118
1227
  normalizeScalingRoadmap(migratedCfg);
1119
1228
  normalizeSessionLifecycle(migratedCfg);
1229
+ normalizeAutoUpdate(migratedCfg);
1120
1230
  const config2 = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
1121
1231
  if (config2.dbPath.startsWith("~")) {
1122
1232
  config2.dbPath = config2.dbPath.replace(/^~/, os.homedir());
@@ -1139,6 +1249,7 @@ function loadConfigSync() {
1139
1249
  const { config: migratedCfg } = migrateConfig(parsed);
1140
1250
  normalizeScalingRoadmap(migratedCfg);
1141
1251
  normalizeSessionLifecycle(migratedCfg);
1252
+ normalizeAutoUpdate(migratedCfg);
1142
1253
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
1143
1254
  } catch {
1144
1255
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
@@ -1158,6 +1269,7 @@ async function loadConfigFrom(configPath) {
1158
1269
  const { config: migratedCfg } = migrateConfig(parsed);
1159
1270
  normalizeScalingRoadmap(migratedCfg);
1160
1271
  normalizeSessionLifecycle(migratedCfg);
1272
+ normalizeAutoUpdate(migratedCfg);
1161
1273
  return { ...DEFAULT_CONFIG, ...migratedCfg };
1162
1274
  } catch {
1163
1275
  return { ...DEFAULT_CONFIG };
@@ -1229,6 +1341,11 @@ var init_config = __esm({
1229
1341
  idleKillTicksRequired: 3,
1230
1342
  idleKillIntercomAckWindowMs: 1e4,
1231
1343
  maxAutoInstances: 10
1344
+ },
1345
+ autoUpdate: {
1346
+ checkOnBoot: true,
1347
+ autoInstall: false,
1348
+ checkIntervalMs: 24 * 60 * 60 * 1e3
1232
1349
  }
1233
1350
  };
1234
1351
  CONFIG_MIGRATIONS = [
@@ -1815,13 +1932,27 @@ async function ensureShardSchema(client) {
1815
1932
  "ALTER TABLE memories ADD COLUMN document_id TEXT",
1816
1933
  "ALTER TABLE memories ADD COLUMN user_id TEXT",
1817
1934
  "ALTER TABLE memories ADD COLUMN char_offset INTEGER",
1818
- "ALTER TABLE memories ADD COLUMN page_number INTEGER"
1935
+ "ALTER TABLE memories ADD COLUMN page_number INTEGER",
1936
+ // Source provenance columns (must match database.ts)
1937
+ "ALTER TABLE memories ADD COLUMN source_path TEXT",
1938
+ "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
1939
+ "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
1940
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
1819
1941
  ]) {
1820
1942
  try {
1821
1943
  await client.execute(col);
1822
1944
  } catch {
1823
1945
  }
1824
1946
  }
1947
+ for (const idx of [
1948
+ "CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)",
1949
+ "CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL"
1950
+ ]) {
1951
+ try {
1952
+ await client.execute(idx);
1953
+ } catch {
1954
+ }
1955
+ }
1825
1956
  try {
1826
1957
  await client.execute("CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status)");
1827
1958
  } catch {
@@ -1930,8 +2061,11 @@ var store_exports = {};
1930
2061
  __export(store_exports, {
1931
2062
  attachDocumentMetadata: () => attachDocumentMetadata,
1932
2063
  buildWikiScopeFilter: () => buildWikiScopeFilter,
2064
+ classifyTier: () => classifyTier,
1933
2065
  disposeStore: () => disposeStore,
1934
2066
  flushBatch: () => flushBatch,
2067
+ flushTier3: () => flushTier3,
2068
+ getMemoryCardinality: () => getMemoryCardinality,
1935
2069
  initStore: () => initStore,
1936
2070
  reserveVersions: () => reserveVersions,
1937
2071
  searchMemories: () => searchMemories,
@@ -1977,6 +2111,11 @@ async function initStore(options) {
1977
2111
  const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1978
2112
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1979
2113
  }
2114
+ function classifyTier(record) {
2115
+ if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
2116
+ if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
2117
+ return 3;
2118
+ }
1980
2119
  async function writeMemory(record) {
1981
2120
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
1982
2121
  throw new Error(
@@ -2004,7 +2143,11 @@ async function writeMemory(record) {
2004
2143
  document_id: record.document_id ?? null,
2005
2144
  user_id: record.user_id ?? null,
2006
2145
  char_offset: record.char_offset ?? null,
2007
- page_number: record.page_number ?? null
2146
+ page_number: record.page_number ?? null,
2147
+ source_path: record.source_path ?? null,
2148
+ source_type: record.source_type ?? null,
2149
+ tier: record.tier ?? classifyTier(record),
2150
+ supersedes_id: record.supersedes_id ?? null
2008
2151
  };
2009
2152
  _pendingRecords.push(dbRow);
2010
2153
  if (_flushTimer === null) {
@@ -2036,20 +2179,26 @@ async function flushBatch() {
2036
2179
  const userId = row.user_id ?? null;
2037
2180
  const charOffset = row.char_offset ?? null;
2038
2181
  const pageNumber = row.page_number ?? null;
2182
+ const sourcePath = row.source_path ?? null;
2183
+ const sourceType = row.source_type ?? null;
2184
+ const tier = row.tier ?? 3;
2185
+ const supersedesId = row.supersedes_id ?? null;
2039
2186
  return {
2040
2187
  sql: hasVector ? `INSERT OR IGNORE INTO memories
2041
2188
  (id, agent_id, agent_role, session_id, timestamp,
2042
2189
  tool_name, project_name,
2043
2190
  has_error, raw_text, vector, version, task_id, importance, status,
2044
2191
  confidence, last_accessed,
2045
- workspace_id, document_id, user_id, char_offset, page_number)
2046
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
2192
+ workspace_id, document_id, user_id, char_offset, page_number,
2193
+ source_path, source_type, tier, supersedes_id)
2194
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
2047
2195
  (id, agent_id, agent_role, session_id, timestamp,
2048
2196
  tool_name, project_name,
2049
2197
  has_error, raw_text, vector, version, task_id, importance, status,
2050
2198
  confidence, last_accessed,
2051
- workspace_id, document_id, user_id, char_offset, page_number)
2052
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2199
+ workspace_id, document_id, user_id, char_offset, page_number,
2200
+ source_path, source_type, tier, supersedes_id)
2201
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2053
2202
  args: hasVector ? [
2054
2203
  row.id,
2055
2204
  row.agent_id,
@@ -2071,7 +2220,11 @@ async function flushBatch() {
2071
2220
  documentId,
2072
2221
  userId,
2073
2222
  charOffset,
2074
- pageNumber
2223
+ pageNumber,
2224
+ sourcePath,
2225
+ sourceType,
2226
+ tier,
2227
+ supersedesId
2075
2228
  ] : [
2076
2229
  row.id,
2077
2230
  row.agent_id,
@@ -2092,7 +2245,11 @@ async function flushBatch() {
2092
2245
  documentId,
2093
2246
  userId,
2094
2247
  charOffset,
2095
- pageNumber
2248
+ pageNumber,
2249
+ sourcePath,
2250
+ sourceType,
2251
+ tier,
2252
+ supersedesId
2096
2253
  ]
2097
2254
  };
2098
2255
  };
@@ -2166,7 +2323,8 @@ async function searchMemories(queryVector, agentId, options) {
2166
2323
  has_error, raw_text, vector, importance, status,
2167
2324
  confidence, last_accessed,
2168
2325
  workspace_id, document_id, user_id,
2169
- char_offset, page_number
2326
+ char_offset, page_number,
2327
+ source_path, source_type
2170
2328
  FROM memories
2171
2329
  WHERE agent_id = ?
2172
2330
  AND vector IS NOT NULL${statusFilter}
@@ -2215,7 +2373,9 @@ async function searchMemories(queryVector, agentId, options) {
2215
2373
  document_id: row.document_id ?? null,
2216
2374
  user_id: row.user_id ?? null,
2217
2375
  char_offset: row.char_offset ?? null,
2218
- page_number: row.page_number ?? null
2376
+ page_number: row.page_number ?? null,
2377
+ source_path: row.source_path ?? null,
2378
+ source_type: row.source_type ?? null
2219
2379
  }));
2220
2380
  }
2221
2381
  async function attachDocumentMetadata(records) {
@@ -2253,6 +2413,25 @@ async function attachDocumentMetadata(records) {
2253
2413
  }
2254
2414
  return records;
2255
2415
  }
2416
+ async function flushTier3(agentId, options) {
2417
+ const client = getClient();
2418
+ const maxAge = options?.maxAgeHours ?? 72;
2419
+ const cutoff = new Date(Date.now() - maxAge * 36e5).toISOString();
2420
+ if (options?.dryRun) {
2421
+ const result2 = await client.execute({
2422
+ sql: `SELECT COUNT(*) as cnt FROM memories
2423
+ WHERE agent_id = ? AND tier = 3 AND status = 'active' AND timestamp < ?`,
2424
+ args: [agentId, cutoff]
2425
+ });
2426
+ return { archived: Number(result2.rows[0]?.cnt ?? 0) };
2427
+ }
2428
+ const result = await client.execute({
2429
+ sql: `UPDATE memories SET status = 'archived'
2430
+ WHERE agent_id = ? AND tier = 3 AND status = 'active' AND timestamp < ?`,
2431
+ args: [agentId, cutoff]
2432
+ });
2433
+ return { archived: result.rowsAffected };
2434
+ }
2256
2435
  async function disposeStore() {
2257
2436
  if (_flushTimer !== null) {
2258
2437
  clearInterval(_flushTimer);
@@ -2283,6 +2462,18 @@ function reserveVersions(count) {
2283
2462
  }
2284
2463
  return reserved;
2285
2464
  }
2465
+ async function getMemoryCardinality(agentId) {
2466
+ try {
2467
+ const client = getClient();
2468
+ const result = await client.execute({
2469
+ sql: `SELECT COUNT(*) as cnt FROM memories WHERE agent_id = ? AND COALESCE(status, 'active') = 'active'`,
2470
+ args: [agentId]
2471
+ });
2472
+ return Number(result.rows[0]?.cnt) || 0;
2473
+ } catch {
2474
+ return 0;
2475
+ }
2476
+ }
2286
2477
  var _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
2287
2478
  var init_store = __esm({
2288
2479
  "src/lib/store.ts"() {
@@ -2766,11 +2957,12 @@ function queueIntercom(targetSession, reason) {
2766
2957
  }
2767
2958
  writeQueue(queue);
2768
2959
  }
2769
- var QUEUE_PATH, INTERCOM_LOG;
2960
+ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
2770
2961
  var init_intercom_queue = __esm({
2771
2962
  "src/lib/intercom-queue.ts"() {
2772
2963
  "use strict";
2773
2964
  QUEUE_PATH = path7.join(os3.homedir(), ".exe-os", "intercom-queue.json");
2965
+ TTL_MS = 60 * 60 * 1e3;
2774
2966
  INTERCOM_LOG = path7.join(os3.homedir(), ".exe-os", "intercom.log");
2775
2967
  }
2776
2968
  });
@@ -2907,6 +3099,17 @@ function getGitRoot(dir) {
2907
3099
  return null;
2908
3100
  }
2909
3101
  }
3102
+ function getMainRepoRoot(dir) {
3103
+ try {
3104
+ const commonDir = execSync4(
3105
+ "git rev-parse --path-format=absolute --git-common-dir",
3106
+ { cwd: dir, encoding: "utf-8", timeout: GIT_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"] }
3107
+ ).trim();
3108
+ return realpath(path11.dirname(commonDir));
3109
+ } catch {
3110
+ return null;
3111
+ }
3112
+ }
2910
3113
  function worktreePath(repoRoot, employeeName, instance) {
2911
3114
  const label = instanceLabel(employeeName, instance);
2912
3115
  return path11.join(repoRoot, ".worktrees", label);
@@ -3143,6 +3346,11 @@ function getSessionState(sessionName) {
3143
3346
  if (!transport.isAlive(sessionName)) return "offline";
3144
3347
  try {
3145
3348
  const pane = transport.capturePane(sessionName, 5);
3349
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
3350
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
3351
+ return "no_claude";
3352
+ }
3353
+ }
3146
3354
  if (/Running…/.test(pane)) return "tool";
3147
3355
  if (BUSY_PATTERN.test(pane)) return "thinking";
3148
3356
  return "idle";
@@ -3150,10 +3358,6 @@ function getSessionState(sessionName) {
3150
3358
  return "offline";
3151
3359
  }
3152
3360
  }
3153
- function isSessionBusy(sessionName) {
3154
- const state = getSessionState(sessionName);
3155
- return state === "thinking" || state === "tool";
3156
- }
3157
3361
  function isExeSession(sessionName) {
3158
3362
  return /^exe\d*$/.test(sessionName);
3159
3363
  }
@@ -3173,7 +3377,14 @@ function sendIntercom(targetSession) {
3173
3377
  logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
3174
3378
  return "failed";
3175
3379
  }
3176
- if (isSessionBusy(targetSession)) {
3380
+ const sessionState = getSessionState(targetSession);
3381
+ if (sessionState === "no_claude") {
3382
+ queueIntercom(targetSession, "claude not running in session");
3383
+ recordDebounce(targetSession);
3384
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
3385
+ return "queued";
3386
+ }
3387
+ if (sessionState === "thinking" || sessionState === "tool") {
3177
3388
  queueIntercom(targetSession, "session busy at send time");
3178
3389
  recordDebounce(targetSession);
3179
3390
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
@@ -3185,18 +3396,7 @@ function sendIntercom(targetSession) {
3185
3396
  }
3186
3397
  transport.sendKeys(targetSession, "/exe-intercom");
3187
3398
  recordDebounce(targetSession);
3188
- for (let i = 0; i < INTERCOM_POLL_MAX_ATTEMPTS; i++) {
3189
- try {
3190
- execSync5(`sleep ${INTERCOM_POLL_INTERVAL_S}`);
3191
- } catch {
3192
- }
3193
- const state = getSessionState(targetSession);
3194
- if (state === "thinking" || state === "tool") {
3195
- logIntercom(`ACKNOWLEDGED \u2192 ${targetSession} (state=${state}, poll=${i + 1})`);
3196
- return "acknowledged";
3197
- }
3198
- }
3199
- logIntercom(`DELIVERED \u2192 ${targetSession} (no state transition after ${INTERCOM_POLL_MAX_ATTEMPTS}s)`);
3399
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
3200
3400
  return "delivered";
3201
3401
  } catch {
3202
3402
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -3213,7 +3413,17 @@ function notifyParentExe(sessionKey) {
3213
3413
  process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
3214
3414
  `);
3215
3415
  const result = sendIntercom(target);
3216
- return result !== "failed";
3416
+ if (result === "failed") {
3417
+ const rootExe = resolveExeSession();
3418
+ if (rootExe && rootExe !== target) {
3419
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
3420
+ `);
3421
+ const fallback = sendIntercom(rootExe);
3422
+ return fallback !== "failed";
3423
+ }
3424
+ return false;
3425
+ }
3426
+ return true;
3217
3427
  }
3218
3428
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3219
3429
  if (employeeName === "exe") {
@@ -3262,7 +3472,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3262
3472
  return { status: "failed", sessionName, error: "intercom delivery failed" };
3263
3473
  }
3264
3474
  const spawnOpts = { ...opts, instance: effectiveInstance };
3265
- const wtPath = ensureWorktree(projectDir, employeeName, effectiveInstance);
3475
+ const mainRoot = getMainRepoRoot(projectDir) ?? projectDir;
3476
+ const wtPath = ensureWorktree(mainRoot, employeeName, effectiveInstance);
3266
3477
  if (wtPath) {
3267
3478
  spawnOpts.cwd = wtPath;
3268
3479
  }
@@ -3443,7 +3654,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3443
3654
  let booted = false;
3444
3655
  for (let i = 0; i < 30; i++) {
3445
3656
  try {
3446
- execSync5("sleep 1");
3657
+ execSync5("sleep 0.5");
3447
3658
  } catch {
3448
3659
  }
3449
3660
  try {
@@ -3463,7 +3674,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3463
3674
  }
3464
3675
  }
3465
3676
  if (!booted) {
3466
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 30s` };
3677
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3467
3678
  }
3468
3679
  if (!useExeAgent) {
3469
3680
  try {
@@ -3481,7 +3692,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3481
3692
  });
3482
3693
  return { sessionName };
3483
3694
  }
3484
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN, INTERCOM_POLL_INTERVAL_S, INTERCOM_POLL_MAX_ATTEMPTS;
3695
+ var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3485
3696
  var init_tmux_routing = __esm({
3486
3697
  "src/lib/tmux-routing.ts"() {
3487
3698
  "use strict";
@@ -3501,8 +3712,6 @@ var init_tmux_routing = __esm({
3501
3712
  DEBOUNCE_FILE = path12.join(SESSION_CACHE, "intercom-debounce.json");
3502
3713
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3503
3714
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
3504
- INTERCOM_POLL_INTERVAL_S = 1;
3505
- INTERCOM_POLL_MAX_ATTEMPTS = 8;
3506
3715
  }
3507
3716
  });
3508
3717
 
@@ -3814,6 +4023,36 @@ import path14 from "path";
3814
4023
  import { execSync as execSync6 } from "child_process";
3815
4024
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
3816
4025
  import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
4026
+ async function writeCheckpoint(input) {
4027
+ const client = getClient();
4028
+ const row = await resolveTask(client, input.taskId);
4029
+ const taskId = String(row.id);
4030
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4031
+ const blockedByIds = [];
4032
+ if (row.blocked_by) {
4033
+ blockedByIds.push(String(row.blocked_by));
4034
+ }
4035
+ const checkpoint = {
4036
+ step: input.step,
4037
+ context_summary: input.contextSummary,
4038
+ files_touched: input.filesTouched ?? [],
4039
+ blocked_by_ids: blockedByIds,
4040
+ last_checkpoint_at: now
4041
+ };
4042
+ const result = await client.execute({
4043
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
4044
+ args: [JSON.stringify(checkpoint), now, taskId]
4045
+ });
4046
+ if (result.rowsAffected === 0) {
4047
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
4048
+ }
4049
+ const countResult = await client.execute({
4050
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
4051
+ args: [taskId]
4052
+ });
4053
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
4054
+ return { checkpointCount };
4055
+ }
3817
4056
  function extractParentFromContext(contextBody) {
3818
4057
  if (!contextBody) return null;
3819
4058
  const match = contextBody.match(
@@ -3920,9 +4159,10 @@ async function createTaskCore(input) {
3920
4159
  } catch {
3921
4160
  }
3922
4161
  }
4162
+ const complexity = input.complexity ?? "standard";
3923
4163
  await client.execute({
3924
- sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, created_at, updated_at)
3925
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4164
+ 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, created_at, updated_at)
4165
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
3926
4166
  args: [
3927
4167
  id,
3928
4168
  input.title,
@@ -3936,6 +4176,11 @@ async function createTaskCore(input) {
3936
4176
  parentTaskId,
3937
4177
  input.reviewer ?? null,
3938
4178
  input.context,
4179
+ input.budgetTokens ?? null,
4180
+ input.budgetFallbackModel ?? null,
4181
+ 0,
4182
+ null,
4183
+ complexity,
3939
4184
  now,
3940
4185
  now
3941
4186
  ]
@@ -3951,7 +4196,11 @@ async function createTaskCore(input) {
3951
4196
  taskFile,
3952
4197
  createdAt: now,
3953
4198
  updatedAt: now,
3954
- warning
4199
+ warning,
4200
+ budgetTokens: input.budgetTokens ?? null,
4201
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
4202
+ tokensUsed: 0,
4203
+ tokensWarnedAt: null
3955
4204
  };
3956
4205
  }
3957
4206
  async function listTasks(input) {
@@ -3991,7 +4240,12 @@ async function listTasks(input) {
3991
4240
  status: String(r.status),
3992
4241
  taskFile: String(r.task_file),
3993
4242
  createdAt: String(r.created_at),
3994
- updatedAt: String(r.updated_at)
4243
+ updatedAt: String(r.updated_at),
4244
+ checkpointCount: Number(r.checkpoint_count ?? 0),
4245
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
4246
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
4247
+ tokensUsed: Number(r.tokens_used ?? 0),
4248
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
3995
4249
  }));
3996
4250
  }
3997
4251
  function checkStaleCompletion(taskContext, taskCreatedAt) {
@@ -3999,8 +4253,13 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
3999
4253
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
4000
4254
  try {
4001
4255
  const since = new Date(taskCreatedAt).toISOString();
4256
+ const branch = execSync6(
4257
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
4258
+ { encoding: "utf8", timeout: 3e3 }
4259
+ ).trim();
4260
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
4002
4261
  const commitCount = execSync6(
4003
- `git log --oneline --since="${since}" 2>/dev/null | wc -l`,
4262
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
4004
4263
  { encoding: "utf8", timeout: 5e3 }
4005
4264
  ).trim();
4006
4265
  const count = parseInt(commitCount, 10);
@@ -4059,6 +4318,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
4059
4318
  const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
4060
4319
  throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
4061
4320
  }
4321
+ try {
4322
+ await writeCheckpoint({
4323
+ taskId,
4324
+ step: "claimed",
4325
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
4326
+ });
4327
+ } catch {
4328
+ }
4062
4329
  return { row, taskFile, now, taskId };
4063
4330
  }
4064
4331
  if (input.result) {
@@ -4072,6 +4339,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
4072
4339
  args: [input.status, now, taskId]
4073
4340
  });
4074
4341
  }
4342
+ try {
4343
+ await writeCheckpoint({
4344
+ taskId,
4345
+ step: `status_transition:${input.status}`,
4346
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
4347
+ });
4348
+ } catch {
4349
+ }
4075
4350
  return { row, taskFile, now, taskId };
4076
4351
  }
4077
4352
  async function deleteTaskCore(taskId, _baseDir) {
@@ -4225,23 +4500,38 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
4225
4500
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
4226
4501
  try {
4227
4502
  const client = getClient();
4228
- const fileName = taskFile.split("/").pop() ?? "";
4229
- const reviewPrefix = fileName.replace(".md", "");
4230
- const parts = reviewPrefix.split("-");
4231
- if (parts.length >= 3 && parts[0] === "review") {
4232
- const agent = parts[1];
4233
- const slug = parts.slice(2).join("-");
4234
- const originalTaskFile = `exe/${agent}/${slug}.md`;
4503
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4504
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
4505
+ if (parentId) {
4235
4506
  const result = await client.execute({
4236
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
4237
- args: [(/* @__PURE__ */ new Date()).toISOString(), originalTaskFile]
4507
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
4508
+ args: [now, parentId]
4238
4509
  });
4239
4510
  if (result.rowsAffected > 0) {
4240
4511
  process.stderr.write(
4241
- `[review-cleanup] Cascaded original task to done: ${originalTaskFile}
4512
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
4242
4513
  `
4243
4514
  );
4244
4515
  }
4516
+ } else {
4517
+ const fileName = taskFile.split("/").pop() ?? "";
4518
+ const reviewPrefix = fileName.replace(".md", "");
4519
+ const parts = reviewPrefix.split("-");
4520
+ if (parts.length >= 3 && parts[0] === "review") {
4521
+ const agent = parts[1];
4522
+ const slug = parts.slice(2).join("-");
4523
+ const originalTaskFile = `exe/${agent}/${slug}.md`;
4524
+ const result = await client.execute({
4525
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
4526
+ args: [now, originalTaskFile]
4527
+ });
4528
+ if (result.rowsAffected > 0) {
4529
+ process.stderr.write(
4530
+ `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
4531
+ `
4532
+ );
4533
+ }
4534
+ }
4245
4535
  }
4246
4536
  } catch (err) {
4247
4537
  process.stderr.write(
@@ -4362,12 +4652,23 @@ function getProjectName(cwd) {
4362
4652
  const dir = cwd ?? process.cwd();
4363
4653
  if (_cached2 && _cachedCwd === dir) return _cached2;
4364
4654
  try {
4365
- const repoRoot = execSync7("git rev-parse --show-toplevel", {
4366
- cwd: dir,
4367
- encoding: "utf8",
4368
- timeout: 2e3,
4369
- stdio: ["pipe", "pipe", "pipe"]
4370
- }).trim();
4655
+ let repoRoot;
4656
+ try {
4657
+ const gitCommonDir = execSync7("git rev-parse --path-format=absolute --git-common-dir", {
4658
+ cwd: dir,
4659
+ encoding: "utf8",
4660
+ timeout: 2e3,
4661
+ stdio: ["pipe", "pipe", "pipe"]
4662
+ }).trim();
4663
+ repoRoot = path17.dirname(gitCommonDir);
4664
+ } catch {
4665
+ repoRoot = execSync7("git rev-parse --show-toplevel", {
4666
+ cwd: dir,
4667
+ encoding: "utf8",
4668
+ timeout: 2e3,
4669
+ stdio: ["pipe", "pipe", "pipe"]
4670
+ }).trim();
4671
+ }
4371
4672
  _cached2 = path17.basename(repoRoot);
4372
4673
  _cachedCwd = dir;
4373
4674
  return _cached2;
@@ -4473,7 +4774,9 @@ async function dispatchTaskToEmployee(input) {
4473
4774
  return { dispatched, session: sessionName, crossProject };
4474
4775
  } else {
4475
4776
  const projectDir = input.projectDir ?? process.cwd();
4476
- const result = ensureEmployee(input.assignedTo, exeSession, projectDir);
4777
+ const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
4778
+ autoInstance: input.assignedTo === "tom" || input.assignedTo === "sasha"
4779
+ });
4477
4780
  if (result.status === "failed") {
4478
4781
  process.stderr.write(
4479
4782
  `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
@@ -4837,7 +5140,8 @@ __export(tasks_exports, {
4837
5140
  resolveTask: () => resolveTask,
4838
5141
  slugify: () => slugify,
4839
5142
  updateTask: () => updateTask,
4840
- updateTaskStatus: () => updateTaskStatus
5143
+ updateTaskStatus: () => updateTaskStatus,
5144
+ writeCheckpoint: () => writeCheckpoint
4841
5145
  });
4842
5146
  import path18 from "path";
4843
5147
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync8, unlinkSync as unlinkSync4 } from "fs";
@@ -4879,10 +5183,11 @@ async function updateTask(input) {
4879
5183
  try {
4880
5184
  const client = getClient();
4881
5185
  const taskTitle = String(row.title);
5186
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
4882
5187
  await client.execute({
4883
5188
  sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
4884
- WHERE title LIKE ? AND status IN ('open', 'in_progress')`,
4885
- args: [now, `%left%${taskTitle}%in_progress`]
5189
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
5190
+ args: [now, `%left '${escaped}' as in\\_progress%`]
4886
5191
  });
4887
5192
  } catch {
4888
5193
  }
@@ -4940,6 +5245,10 @@ async function updateTask(input) {
4940
5245
  taskFile,
4941
5246
  createdAt: String(row.created_at),
4942
5247
  updatedAt: now,
5248
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
5249
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
5250
+ tokensUsed: Number(row.tokens_used ?? 0),
5251
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
4943
5252
  nextTask
4944
5253
  };
4945
5254
  }
@@ -8680,7 +8989,92 @@ async function executeCreateTask(params) {
8680
8989
  baseDir: process.cwd()
8681
8990
  });
8682
8991
  }
8683
- async function executeAction(action, record, executor) {
8992
+ async function executeUpdateWiki(params) {
8993
+ const apiUrl = process.env.EXE_WIKI_API_URL;
8994
+ const apiKey = process.env.EXE_WIKI_API_KEY;
8995
+ if (!apiUrl || !apiKey) {
8996
+ throw new Error("Wiki not configured: EXE_WIKI_API_URL / EXE_WIKI_API_KEY not set");
8997
+ }
8998
+ const workspace = params.workspace;
8999
+ const content = params.content ?? params.text;
9000
+ const mode = params.mode ?? "append";
9001
+ const section = params.section;
9002
+ if (!workspace || !content) {
9003
+ throw new Error("update_wiki requires 'workspace' and 'content' params");
9004
+ }
9005
+ const documentId = params.document_id;
9006
+ if (documentId && mode === "append") {
9007
+ const readRes = await fetch(`${apiUrl}/v1/document/${documentId}`, {
9008
+ headers: { Authorization: `Bearer ${apiKey}` },
9009
+ signal: AbortSignal.timeout(15e3)
9010
+ });
9011
+ if (!readRes.ok) {
9012
+ throw new Error(`Wiki read failed (${readRes.status})`);
9013
+ }
9014
+ const doc = await readRes.json();
9015
+ const existingContent = String(doc.content ?? "");
9016
+ const title = String(doc.title ?? "Untitled");
9017
+ await fetch(`${apiUrl}/v1/document/${documentId}`, {
9018
+ method: "DELETE",
9019
+ headers: { Authorization: `Bearer ${apiKey}` },
9020
+ signal: AbortSignal.timeout(15e3)
9021
+ }).catch(() => {
9022
+ });
9023
+ const uploadRes = await fetch(`${apiUrl}/v1/document/raw-text`, {
9024
+ method: "POST",
9025
+ headers: {
9026
+ "Content-Type": "application/json",
9027
+ Authorization: `Bearer ${apiKey}`
9028
+ },
9029
+ body: JSON.stringify({
9030
+ textContent: section ? existingContent + `
9031
+
9032
+ ## ${section}
9033
+ ${content}` : existingContent + "\n\n" + content,
9034
+ metadata: { title },
9035
+ workspaceSlugs: [workspace]
9036
+ }),
9037
+ signal: AbortSignal.timeout(15e3)
9038
+ });
9039
+ if (!uploadRes.ok) throw new Error(`Wiki upload failed (${uploadRes.status})`);
9040
+ } else {
9041
+ const title = params.title ?? "Auto-generated";
9042
+ const res = await fetch(`${apiUrl}/v1/document/raw-text`, {
9043
+ method: "POST",
9044
+ headers: {
9045
+ "Content-Type": "application/json",
9046
+ Authorization: `Bearer ${apiKey}`
9047
+ },
9048
+ body: JSON.stringify({
9049
+ textContent: content,
9050
+ metadata: { title },
9051
+ workspaceSlugs: [workspace]
9052
+ }),
9053
+ signal: AbortSignal.timeout(15e3)
9054
+ });
9055
+ if (!res.ok) throw new Error(`Wiki create failed (${res.status})`);
9056
+ }
9057
+ }
9058
+ async function routeToApproval(action, resolvedParams, triggerName) {
9059
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
9060
+ const actionSummary = action.type === "send_whatsapp" ? `Send WhatsApp to ${resolvedParams.to ?? resolvedParams.recipient ?? "unknown"}: "${(resolvedParams.message ?? resolvedParams.text ?? "").slice(0, 100)}"` : `${action.type}: ${JSON.stringify(resolvedParams).slice(0, 200)}`;
9061
+ await createTask2({
9062
+ title: `[Approval Required] ${triggerName}: ${action.type}`,
9063
+ assignedTo: "exe",
9064
+ assignedBy: "trigger-engine",
9065
+ projectName: resolvedParams.project ?? "exe-os",
9066
+ priority: "p1",
9067
+ context: `Trigger "${triggerName}" wants to execute this action but requires approval.
9068
+
9069
+ **Action:** ${action.type}
9070
+ **Summary:** ${actionSummary}
9071
+ **Full params:** ${JSON.stringify(resolvedParams, null, 2)}
9072
+
9073
+ To approve, manually run the action via MCP tools.`,
9074
+ baseDir: process.cwd()
9075
+ });
9076
+ }
9077
+ async function executeAction(action, record, executor, triggerName) {
8684
9078
  if (executor) {
8685
9079
  return executor(action, record);
8686
9080
  }
@@ -8688,6 +9082,17 @@ async function executeAction(action, record, executor) {
8688
9082
  for (const [key, val] of Object.entries(action.params)) {
8689
9083
  resolvedParams[key] = substituteTemplate(val, record);
8690
9084
  }
9085
+ if (action.requires_approval) {
9086
+ try {
9087
+ await routeToApproval(action, resolvedParams, triggerName ?? "Unknown trigger");
9088
+ return { success: true };
9089
+ } catch (err) {
9090
+ return {
9091
+ success: false,
9092
+ error: `Approval routing failed: ${err instanceof Error ? err.message : String(err)}`
9093
+ };
9094
+ }
9095
+ }
8691
9096
  try {
8692
9097
  switch (action.type) {
8693
9098
  case "send_whatsapp":
@@ -8699,6 +9104,9 @@ async function executeAction(action, record, executor) {
8699
9104
  case "create_task":
8700
9105
  await executeCreateTask(resolvedParams);
8701
9106
  break;
9107
+ case "update_wiki":
9108
+ await executeUpdateWiki(resolvedParams);
9109
+ break;
8702
9110
  case "mcp_tool":
8703
9111
  console.log(
8704
9112
  `[trigger-engine] mcp_tool action: ${JSON.stringify(resolvedParams)}`
@@ -8723,7 +9131,7 @@ async function processCRMEvent(event, executor, triggersOverride) {
8723
9131
  if (!evaluateConditions(trigger.conditions, event.record)) continue;
8724
9132
  const actionResults = [];
8725
9133
  for (const action of trigger.actions) {
8726
- const result = await executeAction(action, event.record, executor);
9134
+ const result = await executeAction(action, event.record, executor, trigger.name);
8727
9135
  actionResults.push({
8728
9136
  type: action.type,
8729
9137
  success: result.success,