@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
@@ -489,6 +489,27 @@ async function ensureSchema() {
489
489
  });
490
490
  } catch {
491
491
  }
492
+ try {
493
+ await client.execute({
494
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint TEXT`,
495
+ args: []
496
+ });
497
+ } catch {
498
+ }
499
+ try {
500
+ await client.execute({
501
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER NOT NULL DEFAULT 0`,
502
+ args: []
503
+ });
504
+ } catch {
505
+ }
506
+ try {
507
+ await client.execute({
508
+ sql: `ALTER TABLE tasks ADD COLUMN complexity TEXT NOT NULL DEFAULT 'standard'`,
509
+ args: []
510
+ });
511
+ } catch {
512
+ }
492
513
  try {
493
514
  await client.execute({
494
515
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -899,6 +920,15 @@ async function ensureSchema() {
899
920
  } catch {
900
921
  }
901
922
  }
923
+ for (const col of [
924
+ "ALTER TABLE memories ADD COLUMN source_path TEXT",
925
+ "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'"
926
+ ]) {
927
+ try {
928
+ await client.execute(col);
929
+ } catch {
930
+ }
931
+ }
902
932
  await client.executeMultiple(`
903
933
  CREATE INDEX IF NOT EXISTS idx_memories_workspace
904
934
  ON memories(workspace_id);
@@ -963,6 +993,34 @@ async function ensureSchema() {
963
993
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
964
994
  ON conversations(channel_id);
965
995
  `);
996
+ try {
997
+ await client.execute({
998
+ sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
999
+ args: []
1000
+ });
1001
+ } catch {
1002
+ }
1003
+ try {
1004
+ await client.execute({
1005
+ sql: `ALTER TABLE tasks ADD COLUMN budget_fallback_model TEXT`,
1006
+ args: []
1007
+ });
1008
+ } catch {
1009
+ }
1010
+ try {
1011
+ await client.execute({
1012
+ sql: `ALTER TABLE tasks ADD COLUMN tokens_used INTEGER DEFAULT 0`,
1013
+ args: []
1014
+ });
1015
+ } catch {
1016
+ }
1017
+ try {
1018
+ await client.execute({
1019
+ sql: `ALTER TABLE tasks ADD COLUMN tokens_warned_at INTEGER`,
1020
+ args: []
1021
+ });
1022
+ } catch {
1023
+ }
966
1024
  await client.executeMultiple(`
967
1025
  CREATE VIRTUAL TABLE IF NOT EXISTS conversations_fts USING fts5(
968
1026
  content_text,
@@ -989,6 +1047,52 @@ async function ensureSchema() {
989
1047
  VALUES (new.rowid, new.content_text, new.sender_name, new.agent_response);
990
1048
  END;
991
1049
  `);
1050
+ try {
1051
+ await client.execute({
1052
+ sql: `ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3`,
1053
+ args: []
1054
+ });
1055
+ } catch {
1056
+ }
1057
+ try {
1058
+ await client.execute(
1059
+ `CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)`
1060
+ );
1061
+ } catch {
1062
+ }
1063
+ try {
1064
+ await client.execute({
1065
+ sql: `UPDATE memories SET tier = 1 WHERE tool_name = 'commit_to_long_term_memory' AND importance >= 8 AND tier = 3`,
1066
+ args: []
1067
+ });
1068
+ await client.execute({
1069
+ sql: `UPDATE memories SET tier = 2 WHERE tool_name IN ('store_memory', 'manual') AND importance >= 5 AND tier = 3`,
1070
+ args: []
1071
+ });
1072
+ } catch {
1073
+ }
1074
+ try {
1075
+ await client.execute({
1076
+ sql: `ALTER TABLE memories ADD COLUMN supersedes_id TEXT`,
1077
+ args: []
1078
+ });
1079
+ } catch {
1080
+ }
1081
+ try {
1082
+ await client.execute(
1083
+ `CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL`
1084
+ );
1085
+ } catch {
1086
+ }
1087
+ for (const col of [
1088
+ "ALTER TABLE tasks ADD COLUMN checkpoint TEXT",
1089
+ "ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER DEFAULT 0"
1090
+ ]) {
1091
+ try {
1092
+ await client.execute(col);
1093
+ } catch {
1094
+ }
1095
+ }
992
1096
  }
993
1097
  async function disposeDatabase() {
994
1098
  if (_client) {
@@ -1096,6 +1200,11 @@ function normalizeSessionLifecycle(raw) {
1096
1200
  const userSL = raw.sessionLifecycle ?? {};
1097
1201
  raw.sessionLifecycle = { ...defaultSL, ...userSL };
1098
1202
  }
1203
+ function normalizeAutoUpdate(raw) {
1204
+ const defaultAU = DEFAULT_CONFIG.autoUpdate;
1205
+ const userAU = raw.autoUpdate ?? {};
1206
+ raw.autoUpdate = { ...defaultAU, ...userAU };
1207
+ }
1099
1208
  async function loadConfig() {
1100
1209
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1101
1210
  await mkdir(dir, { recursive: true });
@@ -1118,6 +1227,7 @@ async function loadConfig() {
1118
1227
  }
1119
1228
  normalizeScalingRoadmap(migratedCfg);
1120
1229
  normalizeSessionLifecycle(migratedCfg);
1230
+ normalizeAutoUpdate(migratedCfg);
1121
1231
  const config2 = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
1122
1232
  if (config2.dbPath.startsWith("~")) {
1123
1233
  config2.dbPath = config2.dbPath.replace(/^~/, os.homedir());
@@ -1140,6 +1250,7 @@ function loadConfigSync() {
1140
1250
  const { config: migratedCfg } = migrateConfig(parsed);
1141
1251
  normalizeScalingRoadmap(migratedCfg);
1142
1252
  normalizeSessionLifecycle(migratedCfg);
1253
+ normalizeAutoUpdate(migratedCfg);
1143
1254
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
1144
1255
  } catch {
1145
1256
  return { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db") };
@@ -1159,6 +1270,7 @@ async function loadConfigFrom(configPath) {
1159
1270
  const { config: migratedCfg } = migrateConfig(parsed);
1160
1271
  normalizeScalingRoadmap(migratedCfg);
1161
1272
  normalizeSessionLifecycle(migratedCfg);
1273
+ normalizeAutoUpdate(migratedCfg);
1162
1274
  return { ...DEFAULT_CONFIG, ...migratedCfg };
1163
1275
  } catch {
1164
1276
  return { ...DEFAULT_CONFIG };
@@ -1230,6 +1342,11 @@ var init_config = __esm({
1230
1342
  idleKillTicksRequired: 3,
1231
1343
  idleKillIntercomAckWindowMs: 1e4,
1232
1344
  maxAutoInstances: 10
1345
+ },
1346
+ autoUpdate: {
1347
+ checkOnBoot: true,
1348
+ autoInstall: false,
1349
+ checkIntervalMs: 24 * 60 * 60 * 1e3
1233
1350
  }
1234
1351
  };
1235
1352
  CONFIG_MIGRATIONS = [
@@ -1816,13 +1933,27 @@ async function ensureShardSchema(client) {
1816
1933
  "ALTER TABLE memories ADD COLUMN document_id TEXT",
1817
1934
  "ALTER TABLE memories ADD COLUMN user_id TEXT",
1818
1935
  "ALTER TABLE memories ADD COLUMN char_offset INTEGER",
1819
- "ALTER TABLE memories ADD COLUMN page_number INTEGER"
1936
+ "ALTER TABLE memories ADD COLUMN page_number INTEGER",
1937
+ // Source provenance columns (must match database.ts)
1938
+ "ALTER TABLE memories ADD COLUMN source_path TEXT",
1939
+ "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
1940
+ "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
1941
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
1820
1942
  ]) {
1821
1943
  try {
1822
1944
  await client.execute(col);
1823
1945
  } catch {
1824
1946
  }
1825
1947
  }
1948
+ for (const idx of [
1949
+ "CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)",
1950
+ "CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL"
1951
+ ]) {
1952
+ try {
1953
+ await client.execute(idx);
1954
+ } catch {
1955
+ }
1956
+ }
1826
1957
  try {
1827
1958
  await client.execute("CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status)");
1828
1959
  } catch {
@@ -1931,8 +2062,11 @@ var store_exports = {};
1931
2062
  __export(store_exports, {
1932
2063
  attachDocumentMetadata: () => attachDocumentMetadata,
1933
2064
  buildWikiScopeFilter: () => buildWikiScopeFilter,
2065
+ classifyTier: () => classifyTier,
1934
2066
  disposeStore: () => disposeStore,
1935
2067
  flushBatch: () => flushBatch,
2068
+ flushTier3: () => flushTier3,
2069
+ getMemoryCardinality: () => getMemoryCardinality,
1936
2070
  initStore: () => initStore,
1937
2071
  reserveVersions: () => reserveVersions,
1938
2072
  searchMemories: () => searchMemories,
@@ -1978,6 +2112,11 @@ async function initStore(options) {
1978
2112
  const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1979
2113
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1980
2114
  }
2115
+ function classifyTier(record) {
2116
+ if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
2117
+ if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
2118
+ return 3;
2119
+ }
1981
2120
  async function writeMemory(record) {
1982
2121
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
1983
2122
  throw new Error(
@@ -2005,7 +2144,11 @@ async function writeMemory(record) {
2005
2144
  document_id: record.document_id ?? null,
2006
2145
  user_id: record.user_id ?? null,
2007
2146
  char_offset: record.char_offset ?? null,
2008
- page_number: record.page_number ?? null
2147
+ page_number: record.page_number ?? null,
2148
+ source_path: record.source_path ?? null,
2149
+ source_type: record.source_type ?? null,
2150
+ tier: record.tier ?? classifyTier(record),
2151
+ supersedes_id: record.supersedes_id ?? null
2009
2152
  };
2010
2153
  _pendingRecords.push(dbRow);
2011
2154
  if (_flushTimer === null) {
@@ -2037,20 +2180,26 @@ async function flushBatch() {
2037
2180
  const userId = row.user_id ?? null;
2038
2181
  const charOffset = row.char_offset ?? null;
2039
2182
  const pageNumber = row.page_number ?? null;
2183
+ const sourcePath = row.source_path ?? null;
2184
+ const sourceType = row.source_type ?? null;
2185
+ const tier = row.tier ?? 3;
2186
+ const supersedesId = row.supersedes_id ?? null;
2040
2187
  return {
2041
2188
  sql: hasVector ? `INSERT OR IGNORE INTO memories
2042
2189
  (id, agent_id, agent_role, session_id, timestamp,
2043
2190
  tool_name, project_name,
2044
2191
  has_error, raw_text, vector, version, task_id, importance, status,
2045
2192
  confidence, last_accessed,
2046
- workspace_id, document_id, user_id, char_offset, page_number)
2047
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
2193
+ workspace_id, document_id, user_id, char_offset, page_number,
2194
+ source_path, source_type, tier, supersedes_id)
2195
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
2048
2196
  (id, agent_id, agent_role, session_id, timestamp,
2049
2197
  tool_name, project_name,
2050
2198
  has_error, raw_text, vector, version, task_id, importance, status,
2051
2199
  confidence, last_accessed,
2052
- workspace_id, document_id, user_id, char_offset, page_number)
2053
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2200
+ workspace_id, document_id, user_id, char_offset, page_number,
2201
+ source_path, source_type, tier, supersedes_id)
2202
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2054
2203
  args: hasVector ? [
2055
2204
  row.id,
2056
2205
  row.agent_id,
@@ -2072,7 +2221,11 @@ async function flushBatch() {
2072
2221
  documentId,
2073
2222
  userId,
2074
2223
  charOffset,
2075
- pageNumber
2224
+ pageNumber,
2225
+ sourcePath,
2226
+ sourceType,
2227
+ tier,
2228
+ supersedesId
2076
2229
  ] : [
2077
2230
  row.id,
2078
2231
  row.agent_id,
@@ -2093,7 +2246,11 @@ async function flushBatch() {
2093
2246
  documentId,
2094
2247
  userId,
2095
2248
  charOffset,
2096
- pageNumber
2249
+ pageNumber,
2250
+ sourcePath,
2251
+ sourceType,
2252
+ tier,
2253
+ supersedesId
2097
2254
  ]
2098
2255
  };
2099
2256
  };
@@ -2167,7 +2324,8 @@ async function searchMemories(queryVector, agentId, options) {
2167
2324
  has_error, raw_text, vector, importance, status,
2168
2325
  confidence, last_accessed,
2169
2326
  workspace_id, document_id, user_id,
2170
- char_offset, page_number
2327
+ char_offset, page_number,
2328
+ source_path, source_type
2171
2329
  FROM memories
2172
2330
  WHERE agent_id = ?
2173
2331
  AND vector IS NOT NULL${statusFilter}
@@ -2216,7 +2374,9 @@ async function searchMemories(queryVector, agentId, options) {
2216
2374
  document_id: row.document_id ?? null,
2217
2375
  user_id: row.user_id ?? null,
2218
2376
  char_offset: row.char_offset ?? null,
2219
- page_number: row.page_number ?? null
2377
+ page_number: row.page_number ?? null,
2378
+ source_path: row.source_path ?? null,
2379
+ source_type: row.source_type ?? null
2220
2380
  }));
2221
2381
  }
2222
2382
  async function attachDocumentMetadata(records) {
@@ -2254,6 +2414,25 @@ async function attachDocumentMetadata(records) {
2254
2414
  }
2255
2415
  return records;
2256
2416
  }
2417
+ async function flushTier3(agentId, options) {
2418
+ const client = getClient();
2419
+ const maxAge = options?.maxAgeHours ?? 72;
2420
+ const cutoff = new Date(Date.now() - maxAge * 36e5).toISOString();
2421
+ if (options?.dryRun) {
2422
+ const result2 = await client.execute({
2423
+ sql: `SELECT COUNT(*) as cnt FROM memories
2424
+ WHERE agent_id = ? AND tier = 3 AND status = 'active' AND timestamp < ?`,
2425
+ args: [agentId, cutoff]
2426
+ });
2427
+ return { archived: Number(result2.rows[0]?.cnt ?? 0) };
2428
+ }
2429
+ const result = await client.execute({
2430
+ sql: `UPDATE memories SET status = 'archived'
2431
+ WHERE agent_id = ? AND tier = 3 AND status = 'active' AND timestamp < ?`,
2432
+ args: [agentId, cutoff]
2433
+ });
2434
+ return { archived: result.rowsAffected };
2435
+ }
2257
2436
  async function disposeStore() {
2258
2437
  if (_flushTimer !== null) {
2259
2438
  clearInterval(_flushTimer);
@@ -2284,6 +2463,18 @@ function reserveVersions(count) {
2284
2463
  }
2285
2464
  return reserved;
2286
2465
  }
2466
+ async function getMemoryCardinality(agentId) {
2467
+ try {
2468
+ const client = getClient();
2469
+ const result = await client.execute({
2470
+ sql: `SELECT COUNT(*) as cnt FROM memories WHERE agent_id = ? AND COALESCE(status, 'active') = 'active'`,
2471
+ args: [agentId]
2472
+ });
2473
+ return Number(result.rows[0]?.cnt) || 0;
2474
+ } catch {
2475
+ return 0;
2476
+ }
2477
+ }
2287
2478
  var _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
2288
2479
  var init_store = __esm({
2289
2480
  "src/lib/store.ts"() {
@@ -4307,11 +4498,12 @@ function queueIntercom(targetSession, reason) {
4307
4498
  }
4308
4499
  writeQueue(queue);
4309
4500
  }
4310
- var QUEUE_PATH, INTERCOM_LOG;
4501
+ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
4311
4502
  var init_intercom_queue = __esm({
4312
4503
  "src/lib/intercom-queue.ts"() {
4313
4504
  "use strict";
4314
4505
  QUEUE_PATH = path8.join(os3.homedir(), ".exe-os", "intercom-queue.json");
4506
+ TTL_MS = 60 * 60 * 1e3;
4315
4507
  INTERCOM_LOG = path8.join(os3.homedir(), ".exe-os", "intercom.log");
4316
4508
  }
4317
4509
  });
@@ -4425,6 +4617,17 @@ function getGitRoot(dir) {
4425
4617
  return null;
4426
4618
  }
4427
4619
  }
4620
+ function getMainRepoRoot(dir) {
4621
+ try {
4622
+ const commonDir = execSync4(
4623
+ "git rev-parse --path-format=absolute --git-common-dir",
4624
+ { cwd: dir, encoding: "utf-8", timeout: GIT_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"] }
4625
+ ).trim();
4626
+ return realpath(path11.dirname(commonDir));
4627
+ } catch {
4628
+ return null;
4629
+ }
4630
+ }
4428
4631
  function worktreePath(repoRoot, employeeName, instance) {
4429
4632
  const label = instanceLabel(employeeName, instance);
4430
4633
  return path11.join(repoRoot, ".worktrees", label);
@@ -4661,6 +4864,11 @@ function getSessionState(sessionName) {
4661
4864
  if (!transport.isAlive(sessionName)) return "offline";
4662
4865
  try {
4663
4866
  const pane = transport.capturePane(sessionName, 5);
4867
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
4868
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
4869
+ return "no_claude";
4870
+ }
4871
+ }
4664
4872
  if (/Running…/.test(pane)) return "tool";
4665
4873
  if (BUSY_PATTERN.test(pane)) return "thinking";
4666
4874
  return "idle";
@@ -4668,10 +4876,6 @@ function getSessionState(sessionName) {
4668
4876
  return "offline";
4669
4877
  }
4670
4878
  }
4671
- function isSessionBusy(sessionName) {
4672
- const state = getSessionState(sessionName);
4673
- return state === "thinking" || state === "tool";
4674
- }
4675
4879
  function isExeSession(sessionName) {
4676
4880
  return /^exe\d*$/.test(sessionName);
4677
4881
  }
@@ -4691,7 +4895,14 @@ function sendIntercom(targetSession) {
4691
4895
  logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
4692
4896
  return "failed";
4693
4897
  }
4694
- if (isSessionBusy(targetSession)) {
4898
+ const sessionState = getSessionState(targetSession);
4899
+ if (sessionState === "no_claude") {
4900
+ queueIntercom(targetSession, "claude not running in session");
4901
+ recordDebounce(targetSession);
4902
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
4903
+ return "queued";
4904
+ }
4905
+ if (sessionState === "thinking" || sessionState === "tool") {
4695
4906
  queueIntercom(targetSession, "session busy at send time");
4696
4907
  recordDebounce(targetSession);
4697
4908
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
@@ -4703,18 +4914,7 @@ function sendIntercom(targetSession) {
4703
4914
  }
4704
4915
  transport.sendKeys(targetSession, "/exe-intercom");
4705
4916
  recordDebounce(targetSession);
4706
- for (let i = 0; i < INTERCOM_POLL_MAX_ATTEMPTS; i++) {
4707
- try {
4708
- execSync5(`sleep ${INTERCOM_POLL_INTERVAL_S}`);
4709
- } catch {
4710
- }
4711
- const state = getSessionState(targetSession);
4712
- if (state === "thinking" || state === "tool") {
4713
- logIntercom(`ACKNOWLEDGED \u2192 ${targetSession} (state=${state}, poll=${i + 1})`);
4714
- return "acknowledged";
4715
- }
4716
- }
4717
- logIntercom(`DELIVERED \u2192 ${targetSession} (no state transition after ${INTERCOM_POLL_MAX_ATTEMPTS}s)`);
4917
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
4718
4918
  return "delivered";
4719
4919
  } catch {
4720
4920
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -4731,7 +4931,17 @@ function notifyParentExe(sessionKey) {
4731
4931
  process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
4732
4932
  `);
4733
4933
  const result = sendIntercom(target);
4734
- return result !== "failed";
4934
+ if (result === "failed") {
4935
+ const rootExe = resolveExeSession();
4936
+ if (rootExe && rootExe !== target) {
4937
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
4938
+ `);
4939
+ const fallback = sendIntercom(rootExe);
4940
+ return fallback !== "failed";
4941
+ }
4942
+ return false;
4943
+ }
4944
+ return true;
4735
4945
  }
4736
4946
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4737
4947
  if (employeeName === "exe") {
@@ -4780,7 +4990,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
4780
4990
  return { status: "failed", sessionName, error: "intercom delivery failed" };
4781
4991
  }
4782
4992
  const spawnOpts = { ...opts, instance: effectiveInstance };
4783
- const wtPath = ensureWorktree(projectDir, employeeName, effectiveInstance);
4993
+ const mainRoot = getMainRepoRoot(projectDir) ?? projectDir;
4994
+ const wtPath = ensureWorktree(mainRoot, employeeName, effectiveInstance);
4784
4995
  if (wtPath) {
4785
4996
  spawnOpts.cwd = wtPath;
4786
4997
  }
@@ -4961,7 +5172,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4961
5172
  let booted = false;
4962
5173
  for (let i = 0; i < 30; i++) {
4963
5174
  try {
4964
- execSync5("sleep 1");
5175
+ execSync5("sleep 0.5");
4965
5176
  } catch {
4966
5177
  }
4967
5178
  try {
@@ -4981,7 +5192,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4981
5192
  }
4982
5193
  }
4983
5194
  if (!booted) {
4984
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 30s` };
5195
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
4985
5196
  }
4986
5197
  if (!useExeAgent) {
4987
5198
  try {
@@ -4999,7 +5210,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4999
5210
  });
5000
5211
  return { sessionName };
5001
5212
  }
5002
- 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;
5213
+ var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
5003
5214
  var init_tmux_routing = __esm({
5004
5215
  "src/lib/tmux-routing.ts"() {
5005
5216
  "use strict";
@@ -5019,8 +5230,6 @@ var init_tmux_routing = __esm({
5019
5230
  DEBOUNCE_FILE = path12.join(SESSION_CACHE, "intercom-debounce.json");
5020
5231
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
5021
5232
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
5022
- INTERCOM_POLL_INTERVAL_S = 1;
5023
- INTERCOM_POLL_MAX_ATTEMPTS = 8;
5024
5233
  }
5025
5234
  });
5026
5235
 
@@ -5332,6 +5541,36 @@ import path14 from "path";
5332
5541
  import { execSync as execSync6 } from "child_process";
5333
5542
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
5334
5543
  import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
5544
+ async function writeCheckpoint(input) {
5545
+ const client = getClient();
5546
+ const row = await resolveTask(client, input.taskId);
5547
+ const taskId = String(row.id);
5548
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5549
+ const blockedByIds = [];
5550
+ if (row.blocked_by) {
5551
+ blockedByIds.push(String(row.blocked_by));
5552
+ }
5553
+ const checkpoint = {
5554
+ step: input.step,
5555
+ context_summary: input.contextSummary,
5556
+ files_touched: input.filesTouched ?? [],
5557
+ blocked_by_ids: blockedByIds,
5558
+ last_checkpoint_at: now
5559
+ };
5560
+ const result = await client.execute({
5561
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
5562
+ args: [JSON.stringify(checkpoint), now, taskId]
5563
+ });
5564
+ if (result.rowsAffected === 0) {
5565
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
5566
+ }
5567
+ const countResult = await client.execute({
5568
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
5569
+ args: [taskId]
5570
+ });
5571
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
5572
+ return { checkpointCount };
5573
+ }
5335
5574
  function extractParentFromContext(contextBody) {
5336
5575
  if (!contextBody) return null;
5337
5576
  const match = contextBody.match(
@@ -5438,9 +5677,10 @@ async function createTaskCore(input) {
5438
5677
  } catch {
5439
5678
  }
5440
5679
  }
5680
+ const complexity = input.complexity ?? "standard";
5441
5681
  await client.execute({
5442
- 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)
5443
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5682
+ 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)
5683
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
5444
5684
  args: [
5445
5685
  id,
5446
5686
  input.title,
@@ -5454,6 +5694,11 @@ async function createTaskCore(input) {
5454
5694
  parentTaskId,
5455
5695
  input.reviewer ?? null,
5456
5696
  input.context,
5697
+ input.budgetTokens ?? null,
5698
+ input.budgetFallbackModel ?? null,
5699
+ 0,
5700
+ null,
5701
+ complexity,
5457
5702
  now,
5458
5703
  now
5459
5704
  ]
@@ -5469,7 +5714,11 @@ async function createTaskCore(input) {
5469
5714
  taskFile,
5470
5715
  createdAt: now,
5471
5716
  updatedAt: now,
5472
- warning
5717
+ warning,
5718
+ budgetTokens: input.budgetTokens ?? null,
5719
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
5720
+ tokensUsed: 0,
5721
+ tokensWarnedAt: null
5473
5722
  };
5474
5723
  }
5475
5724
  async function listTasks(input) {
@@ -5509,7 +5758,12 @@ async function listTasks(input) {
5509
5758
  status: String(r.status),
5510
5759
  taskFile: String(r.task_file),
5511
5760
  createdAt: String(r.created_at),
5512
- updatedAt: String(r.updated_at)
5761
+ updatedAt: String(r.updated_at),
5762
+ checkpointCount: Number(r.checkpoint_count ?? 0),
5763
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
5764
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
5765
+ tokensUsed: Number(r.tokens_used ?? 0),
5766
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
5513
5767
  }));
5514
5768
  }
5515
5769
  function checkStaleCompletion(taskContext, taskCreatedAt) {
@@ -5517,8 +5771,13 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
5517
5771
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
5518
5772
  try {
5519
5773
  const since = new Date(taskCreatedAt).toISOString();
5774
+ const branch = execSync6(
5775
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
5776
+ { encoding: "utf8", timeout: 3e3 }
5777
+ ).trim();
5778
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
5520
5779
  const commitCount = execSync6(
5521
- `git log --oneline --since="${since}" 2>/dev/null | wc -l`,
5780
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
5522
5781
  { encoding: "utf8", timeout: 5e3 }
5523
5782
  ).trim();
5524
5783
  const count = parseInt(commitCount, 10);
@@ -5577,6 +5836,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
5577
5836
  const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
5578
5837
  throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
5579
5838
  }
5839
+ try {
5840
+ await writeCheckpoint({
5841
+ taskId,
5842
+ step: "claimed",
5843
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
5844
+ });
5845
+ } catch {
5846
+ }
5580
5847
  return { row, taskFile, now, taskId };
5581
5848
  }
5582
5849
  if (input.result) {
@@ -5590,6 +5857,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
5590
5857
  args: [input.status, now, taskId]
5591
5858
  });
5592
5859
  }
5860
+ try {
5861
+ await writeCheckpoint({
5862
+ taskId,
5863
+ step: `status_transition:${input.status}`,
5864
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
5865
+ });
5866
+ } catch {
5867
+ }
5593
5868
  return { row, taskFile, now, taskId };
5594
5869
  }
5595
5870
  async function deleteTaskCore(taskId, _baseDir) {
@@ -5743,23 +6018,38 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
5743
6018
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
5744
6019
  try {
5745
6020
  const client = getClient();
5746
- const fileName = taskFile.split("/").pop() ?? "";
5747
- const reviewPrefix = fileName.replace(".md", "");
5748
- const parts = reviewPrefix.split("-");
5749
- if (parts.length >= 3 && parts[0] === "review") {
5750
- const agent = parts[1];
5751
- const slug = parts.slice(2).join("-");
5752
- const originalTaskFile = `exe/${agent}/${slug}.md`;
6021
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6022
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
6023
+ if (parentId) {
5753
6024
  const result = await client.execute({
5754
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
5755
- args: [(/* @__PURE__ */ new Date()).toISOString(), originalTaskFile]
6025
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
6026
+ args: [now, parentId]
5756
6027
  });
5757
6028
  if (result.rowsAffected > 0) {
5758
6029
  process.stderr.write(
5759
- `[review-cleanup] Cascaded original task to done: ${originalTaskFile}
6030
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
5760
6031
  `
5761
6032
  );
5762
6033
  }
6034
+ } else {
6035
+ const fileName = taskFile.split("/").pop() ?? "";
6036
+ const reviewPrefix = fileName.replace(".md", "");
6037
+ const parts = reviewPrefix.split("-");
6038
+ if (parts.length >= 3 && parts[0] === "review") {
6039
+ const agent = parts[1];
6040
+ const slug = parts.slice(2).join("-");
6041
+ const originalTaskFile = `exe/${agent}/${slug}.md`;
6042
+ const result = await client.execute({
6043
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
6044
+ args: [now, originalTaskFile]
6045
+ });
6046
+ if (result.rowsAffected > 0) {
6047
+ process.stderr.write(
6048
+ `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
6049
+ `
6050
+ );
6051
+ }
6052
+ }
5763
6053
  }
5764
6054
  } catch (err) {
5765
6055
  process.stderr.write(
@@ -5880,12 +6170,23 @@ function getProjectName(cwd) {
5880
6170
  const dir = cwd ?? process.cwd();
5881
6171
  if (_cached2 && _cachedCwd === dir) return _cached2;
5882
6172
  try {
5883
- const repoRoot = execSync7("git rev-parse --show-toplevel", {
5884
- cwd: dir,
5885
- encoding: "utf8",
5886
- timeout: 2e3,
5887
- stdio: ["pipe", "pipe", "pipe"]
5888
- }).trim();
6173
+ let repoRoot;
6174
+ try {
6175
+ const gitCommonDir = execSync7("git rev-parse --path-format=absolute --git-common-dir", {
6176
+ cwd: dir,
6177
+ encoding: "utf8",
6178
+ timeout: 2e3,
6179
+ stdio: ["pipe", "pipe", "pipe"]
6180
+ }).trim();
6181
+ repoRoot = path17.dirname(gitCommonDir);
6182
+ } catch {
6183
+ repoRoot = execSync7("git rev-parse --show-toplevel", {
6184
+ cwd: dir,
6185
+ encoding: "utf8",
6186
+ timeout: 2e3,
6187
+ stdio: ["pipe", "pipe", "pipe"]
6188
+ }).trim();
6189
+ }
5889
6190
  _cached2 = path17.basename(repoRoot);
5890
6191
  _cachedCwd = dir;
5891
6192
  return _cached2;
@@ -5991,7 +6292,9 @@ async function dispatchTaskToEmployee(input) {
5991
6292
  return { dispatched, session: sessionName, crossProject };
5992
6293
  } else {
5993
6294
  const projectDir = input.projectDir ?? process.cwd();
5994
- const result = ensureEmployee(input.assignedTo, exeSession, projectDir);
6295
+ const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
6296
+ autoInstance: input.assignedTo === "tom" || input.assignedTo === "sasha"
6297
+ });
5995
6298
  if (result.status === "failed") {
5996
6299
  process.stderr.write(
5997
6300
  `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
@@ -6355,7 +6658,8 @@ __export(tasks_exports, {
6355
6658
  resolveTask: () => resolveTask,
6356
6659
  slugify: () => slugify,
6357
6660
  updateTask: () => updateTask,
6358
- updateTaskStatus: () => updateTaskStatus
6661
+ updateTaskStatus: () => updateTaskStatus,
6662
+ writeCheckpoint: () => writeCheckpoint
6359
6663
  });
6360
6664
  import path18 from "path";
6361
6665
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync8, unlinkSync as unlinkSync4 } from "fs";
@@ -6397,10 +6701,11 @@ async function updateTask(input) {
6397
6701
  try {
6398
6702
  const client = getClient();
6399
6703
  const taskTitle = String(row.title);
6704
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
6400
6705
  await client.execute({
6401
6706
  sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
6402
- WHERE title LIKE ? AND status IN ('open', 'in_progress')`,
6403
- args: [now, `%left%${taskTitle}%in_progress`]
6707
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
6708
+ args: [now, `%left '${escaped}' as in\\_progress%`]
6404
6709
  });
6405
6710
  } catch {
6406
6711
  }
@@ -6458,6 +6763,10 @@ async function updateTask(input) {
6458
6763
  taskFile,
6459
6764
  createdAt: String(row.created_at),
6460
6765
  updatedAt: now,
6766
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
6767
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
6768
+ tokensUsed: Number(row.tokens_used ?? 0),
6769
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
6461
6770
  nextTask
6462
6771
  };
6463
6772
  }
@@ -6615,7 +6924,92 @@ async function executeCreateTask(params) {
6615
6924
  baseDir: process.cwd()
6616
6925
  });
6617
6926
  }
6618
- async function executeAction(action, record, executor) {
6927
+ async function executeUpdateWiki(params) {
6928
+ const apiUrl = process.env.EXE_WIKI_API_URL;
6929
+ const apiKey = process.env.EXE_WIKI_API_KEY;
6930
+ if (!apiUrl || !apiKey) {
6931
+ throw new Error("Wiki not configured: EXE_WIKI_API_URL / EXE_WIKI_API_KEY not set");
6932
+ }
6933
+ const workspace = params.workspace;
6934
+ const content = params.content ?? params.text;
6935
+ const mode = params.mode ?? "append";
6936
+ const section = params.section;
6937
+ if (!workspace || !content) {
6938
+ throw new Error("update_wiki requires 'workspace' and 'content' params");
6939
+ }
6940
+ const documentId = params.document_id;
6941
+ if (documentId && mode === "append") {
6942
+ const readRes = await fetch(`${apiUrl}/v1/document/${documentId}`, {
6943
+ headers: { Authorization: `Bearer ${apiKey}` },
6944
+ signal: AbortSignal.timeout(15e3)
6945
+ });
6946
+ if (!readRes.ok) {
6947
+ throw new Error(`Wiki read failed (${readRes.status})`);
6948
+ }
6949
+ const doc = await readRes.json();
6950
+ const existingContent = String(doc.content ?? "");
6951
+ const title = String(doc.title ?? "Untitled");
6952
+ await fetch(`${apiUrl}/v1/document/${documentId}`, {
6953
+ method: "DELETE",
6954
+ headers: { Authorization: `Bearer ${apiKey}` },
6955
+ signal: AbortSignal.timeout(15e3)
6956
+ }).catch(() => {
6957
+ });
6958
+ const uploadRes = await fetch(`${apiUrl}/v1/document/raw-text`, {
6959
+ method: "POST",
6960
+ headers: {
6961
+ "Content-Type": "application/json",
6962
+ Authorization: `Bearer ${apiKey}`
6963
+ },
6964
+ body: JSON.stringify({
6965
+ textContent: section ? existingContent + `
6966
+
6967
+ ## ${section}
6968
+ ${content}` : existingContent + "\n\n" + content,
6969
+ metadata: { title },
6970
+ workspaceSlugs: [workspace]
6971
+ }),
6972
+ signal: AbortSignal.timeout(15e3)
6973
+ });
6974
+ if (!uploadRes.ok) throw new Error(`Wiki upload failed (${uploadRes.status})`);
6975
+ } else {
6976
+ const title = params.title ?? "Auto-generated";
6977
+ const res = await fetch(`${apiUrl}/v1/document/raw-text`, {
6978
+ method: "POST",
6979
+ headers: {
6980
+ "Content-Type": "application/json",
6981
+ Authorization: `Bearer ${apiKey}`
6982
+ },
6983
+ body: JSON.stringify({
6984
+ textContent: content,
6985
+ metadata: { title },
6986
+ workspaceSlugs: [workspace]
6987
+ }),
6988
+ signal: AbortSignal.timeout(15e3)
6989
+ });
6990
+ if (!res.ok) throw new Error(`Wiki create failed (${res.status})`);
6991
+ }
6992
+ }
6993
+ async function routeToApproval(action, resolvedParams, triggerName) {
6994
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
6995
+ 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)}`;
6996
+ await createTask2({
6997
+ title: `[Approval Required] ${triggerName}: ${action.type}`,
6998
+ assignedTo: "exe",
6999
+ assignedBy: "trigger-engine",
7000
+ projectName: resolvedParams.project ?? "exe-os",
7001
+ priority: "p1",
7002
+ context: `Trigger "${triggerName}" wants to execute this action but requires approval.
7003
+
7004
+ **Action:** ${action.type}
7005
+ **Summary:** ${actionSummary}
7006
+ **Full params:** ${JSON.stringify(resolvedParams, null, 2)}
7007
+
7008
+ To approve, manually run the action via MCP tools.`,
7009
+ baseDir: process.cwd()
7010
+ });
7011
+ }
7012
+ async function executeAction(action, record, executor, triggerName) {
6619
7013
  if (executor) {
6620
7014
  return executor(action, record);
6621
7015
  }
@@ -6623,6 +7017,17 @@ async function executeAction(action, record, executor) {
6623
7017
  for (const [key, val] of Object.entries(action.params)) {
6624
7018
  resolvedParams[key] = substituteTemplate(val, record);
6625
7019
  }
7020
+ if (action.requires_approval) {
7021
+ try {
7022
+ await routeToApproval(action, resolvedParams, triggerName ?? "Unknown trigger");
7023
+ return { success: true };
7024
+ } catch (err) {
7025
+ return {
7026
+ success: false,
7027
+ error: `Approval routing failed: ${err instanceof Error ? err.message : String(err)}`
7028
+ };
7029
+ }
7030
+ }
6626
7031
  try {
6627
7032
  switch (action.type) {
6628
7033
  case "send_whatsapp":
@@ -6634,6 +7039,9 @@ async function executeAction(action, record, executor) {
6634
7039
  case "create_task":
6635
7040
  await executeCreateTask(resolvedParams);
6636
7041
  break;
7042
+ case "update_wiki":
7043
+ await executeUpdateWiki(resolvedParams);
7044
+ break;
6637
7045
  case "mcp_tool":
6638
7046
  console.log(
6639
7047
  `[trigger-engine] mcp_tool action: ${JSON.stringify(resolvedParams)}`
@@ -6658,7 +7066,7 @@ async function processCRMEvent(event, executor, triggersOverride) {
6658
7066
  if (!evaluateConditions(trigger.conditions, event.record)) continue;
6659
7067
  const actionResults = [];
6660
7068
  for (const action of trigger.actions) {
6661
- const result = await executeAction(action, event.record, executor);
7069
+ const result = await executeAction(action, event.record, executor, trigger.name);
6662
7070
  actionResults.push({
6663
7071
  type: action.type,
6664
7072
  success: result.success,