@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
package/dist/index.js CHANGED
@@ -349,11 +349,12 @@ function queueIntercom(targetSession, reason) {
349
349
  }
350
350
  writeQueue(queue);
351
351
  }
352
- var QUEUE_PATH, INTERCOM_LOG;
352
+ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
353
353
  var init_intercom_queue = __esm({
354
354
  "src/lib/intercom-queue.ts"() {
355
355
  "use strict";
356
356
  QUEUE_PATH = path3.join(os3.homedir(), ".exe-os", "intercom-queue.json");
357
+ TTL_MS = 60 * 60 * 1e3;
357
358
  INTERCOM_LOG = path3.join(os3.homedir(), ".exe-os", "intercom.log");
358
359
  }
359
360
  });
@@ -579,6 +580,27 @@ async function ensureSchema() {
579
580
  });
580
581
  } catch {
581
582
  }
583
+ try {
584
+ await client.execute({
585
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint TEXT`,
586
+ args: []
587
+ });
588
+ } catch {
589
+ }
590
+ try {
591
+ await client.execute({
592
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER NOT NULL DEFAULT 0`,
593
+ args: []
594
+ });
595
+ } catch {
596
+ }
597
+ try {
598
+ await client.execute({
599
+ sql: `ALTER TABLE tasks ADD COLUMN complexity TEXT NOT NULL DEFAULT 'standard'`,
600
+ args: []
601
+ });
602
+ } catch {
603
+ }
582
604
  try {
583
605
  await client.execute({
584
606
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -989,6 +1011,15 @@ async function ensureSchema() {
989
1011
  } catch {
990
1012
  }
991
1013
  }
1014
+ for (const col of [
1015
+ "ALTER TABLE memories ADD COLUMN source_path TEXT",
1016
+ "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'"
1017
+ ]) {
1018
+ try {
1019
+ await client.execute(col);
1020
+ } catch {
1021
+ }
1022
+ }
992
1023
  await client.executeMultiple(`
993
1024
  CREATE INDEX IF NOT EXISTS idx_memories_workspace
994
1025
  ON memories(workspace_id);
@@ -1053,6 +1084,34 @@ async function ensureSchema() {
1053
1084
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
1054
1085
  ON conversations(channel_id);
1055
1086
  `);
1087
+ try {
1088
+ await client.execute({
1089
+ sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
1090
+ args: []
1091
+ });
1092
+ } catch {
1093
+ }
1094
+ try {
1095
+ await client.execute({
1096
+ sql: `ALTER TABLE tasks ADD COLUMN budget_fallback_model TEXT`,
1097
+ args: []
1098
+ });
1099
+ } catch {
1100
+ }
1101
+ try {
1102
+ await client.execute({
1103
+ sql: `ALTER TABLE tasks ADD COLUMN tokens_used INTEGER DEFAULT 0`,
1104
+ args: []
1105
+ });
1106
+ } catch {
1107
+ }
1108
+ try {
1109
+ await client.execute({
1110
+ sql: `ALTER TABLE tasks ADD COLUMN tokens_warned_at INTEGER`,
1111
+ args: []
1112
+ });
1113
+ } catch {
1114
+ }
1056
1115
  await client.executeMultiple(`
1057
1116
  CREATE VIRTUAL TABLE IF NOT EXISTS conversations_fts USING fts5(
1058
1117
  content_text,
@@ -1079,6 +1138,52 @@ async function ensureSchema() {
1079
1138
  VALUES (new.rowid, new.content_text, new.sender_name, new.agent_response);
1080
1139
  END;
1081
1140
  `);
1141
+ try {
1142
+ await client.execute({
1143
+ sql: `ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3`,
1144
+ args: []
1145
+ });
1146
+ } catch {
1147
+ }
1148
+ try {
1149
+ await client.execute(
1150
+ `CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)`
1151
+ );
1152
+ } catch {
1153
+ }
1154
+ try {
1155
+ await client.execute({
1156
+ sql: `UPDATE memories SET tier = 1 WHERE tool_name = 'commit_to_long_term_memory' AND importance >= 8 AND tier = 3`,
1157
+ args: []
1158
+ });
1159
+ await client.execute({
1160
+ sql: `UPDATE memories SET tier = 2 WHERE tool_name IN ('store_memory', 'manual') AND importance >= 5 AND tier = 3`,
1161
+ args: []
1162
+ });
1163
+ } catch {
1164
+ }
1165
+ try {
1166
+ await client.execute({
1167
+ sql: `ALTER TABLE memories ADD COLUMN supersedes_id TEXT`,
1168
+ args: []
1169
+ });
1170
+ } catch {
1171
+ }
1172
+ try {
1173
+ await client.execute(
1174
+ `CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL`
1175
+ );
1176
+ } catch {
1177
+ }
1178
+ for (const col of [
1179
+ "ALTER TABLE tasks ADD COLUMN checkpoint TEXT",
1180
+ "ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER DEFAULT 0"
1181
+ ]) {
1182
+ try {
1183
+ await client.execute(col);
1184
+ } catch {
1185
+ }
1186
+ }
1082
1187
  }
1083
1188
  async function disposeDatabase() {
1084
1189
  if (_client) {
@@ -1177,6 +1282,11 @@ function normalizeSessionLifecycle(raw) {
1177
1282
  const userSL = raw.sessionLifecycle ?? {};
1178
1283
  raw.sessionLifecycle = { ...defaultSL, ...userSL };
1179
1284
  }
1285
+ function normalizeAutoUpdate(raw) {
1286
+ const defaultAU = DEFAULT_CONFIG.autoUpdate;
1287
+ const userAU = raw.autoUpdate ?? {};
1288
+ raw.autoUpdate = { ...defaultAU, ...userAU };
1289
+ }
1180
1290
  async function loadConfig() {
1181
1291
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
1182
1292
  await mkdir(dir, { recursive: true });
@@ -1199,6 +1309,7 @@ async function loadConfig() {
1199
1309
  }
1200
1310
  normalizeScalingRoadmap(migratedCfg);
1201
1311
  normalizeSessionLifecycle(migratedCfg);
1312
+ normalizeAutoUpdate(migratedCfg);
1202
1313
  const config2 = { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db"), ...migratedCfg };
1203
1314
  if (config2.dbPath.startsWith("~")) {
1204
1315
  config2.dbPath = config2.dbPath.replace(/^~/, os4.homedir());
@@ -1221,6 +1332,7 @@ function loadConfigSync() {
1221
1332
  const { config: migratedCfg } = migrateConfig(parsed);
1222
1333
  normalizeScalingRoadmap(migratedCfg);
1223
1334
  normalizeSessionLifecycle(migratedCfg);
1335
+ normalizeAutoUpdate(migratedCfg);
1224
1336
  return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db"), ...migratedCfg };
1225
1337
  } catch {
1226
1338
  return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db") };
@@ -1240,6 +1352,7 @@ async function loadConfigFrom(configPath) {
1240
1352
  const { config: migratedCfg } = migrateConfig(parsed);
1241
1353
  normalizeScalingRoadmap(migratedCfg);
1242
1354
  normalizeSessionLifecycle(migratedCfg);
1355
+ normalizeAutoUpdate(migratedCfg);
1243
1356
  return { ...DEFAULT_CONFIG, ...migratedCfg };
1244
1357
  } catch {
1245
1358
  return { ...DEFAULT_CONFIG };
@@ -1311,6 +1424,11 @@ var init_config = __esm({
1311
1424
  idleKillTicksRequired: 3,
1312
1425
  idleKillIntercomAckWindowMs: 1e4,
1313
1426
  maxAutoInstances: 10
1427
+ },
1428
+ autoUpdate: {
1429
+ checkOnBoot: true,
1430
+ autoInstall: false,
1431
+ checkIntervalMs: 24 * 60 * 60 * 1e3
1314
1432
  }
1315
1433
  };
1316
1434
  CONFIG_MIGRATIONS = [
@@ -1458,6 +1576,17 @@ function getGitRoot(dir) {
1458
1576
  return null;
1459
1577
  }
1460
1578
  }
1579
+ function getMainRepoRoot(dir) {
1580
+ try {
1581
+ const commonDir = execSync5(
1582
+ "git rev-parse --path-format=absolute --git-common-dir",
1583
+ { cwd: dir, encoding: "utf-8", timeout: GIT_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"] }
1584
+ ).trim();
1585
+ return realpath(path8.dirname(commonDir));
1586
+ } catch {
1587
+ return null;
1588
+ }
1589
+ }
1461
1590
  function worktreePath(repoRoot, employeeName, instance) {
1462
1591
  const label = instanceLabel(employeeName, instance);
1463
1592
  return path8.join(repoRoot, ".worktrees", label);
@@ -1649,6 +1778,36 @@ import path10 from "path";
1649
1778
  import { execSync as execSync6 } from "child_process";
1650
1779
  import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
1651
1780
  import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
1781
+ async function writeCheckpoint(input) {
1782
+ const client = getClient();
1783
+ const row = await resolveTask(client, input.taskId);
1784
+ const taskId = String(row.id);
1785
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1786
+ const blockedByIds = [];
1787
+ if (row.blocked_by) {
1788
+ blockedByIds.push(String(row.blocked_by));
1789
+ }
1790
+ const checkpoint = {
1791
+ step: input.step,
1792
+ context_summary: input.contextSummary,
1793
+ files_touched: input.filesTouched ?? [],
1794
+ blocked_by_ids: blockedByIds,
1795
+ last_checkpoint_at: now
1796
+ };
1797
+ const result = await client.execute({
1798
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
1799
+ args: [JSON.stringify(checkpoint), now, taskId]
1800
+ });
1801
+ if (result.rowsAffected === 0) {
1802
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
1803
+ }
1804
+ const countResult = await client.execute({
1805
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
1806
+ args: [taskId]
1807
+ });
1808
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
1809
+ return { checkpointCount };
1810
+ }
1652
1811
  function extractParentFromContext(contextBody) {
1653
1812
  if (!contextBody) return null;
1654
1813
  const match = contextBody.match(
@@ -1755,9 +1914,10 @@ async function createTaskCore(input) {
1755
1914
  } catch {
1756
1915
  }
1757
1916
  }
1917
+ const complexity = input.complexity ?? "standard";
1758
1918
  await client.execute({
1759
- 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)
1760
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1919
+ 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)
1920
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1761
1921
  args: [
1762
1922
  id,
1763
1923
  input.title,
@@ -1771,6 +1931,11 @@ async function createTaskCore(input) {
1771
1931
  parentTaskId,
1772
1932
  input.reviewer ?? null,
1773
1933
  input.context,
1934
+ input.budgetTokens ?? null,
1935
+ input.budgetFallbackModel ?? null,
1936
+ 0,
1937
+ null,
1938
+ complexity,
1774
1939
  now,
1775
1940
  now
1776
1941
  ]
@@ -1786,7 +1951,11 @@ async function createTaskCore(input) {
1786
1951
  taskFile,
1787
1952
  createdAt: now,
1788
1953
  updatedAt: now,
1789
- warning
1954
+ warning,
1955
+ budgetTokens: input.budgetTokens ?? null,
1956
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
1957
+ tokensUsed: 0,
1958
+ tokensWarnedAt: null
1790
1959
  };
1791
1960
  }
1792
1961
  async function listTasks(input) {
@@ -1826,7 +1995,12 @@ async function listTasks(input) {
1826
1995
  status: String(r.status),
1827
1996
  taskFile: String(r.task_file),
1828
1997
  createdAt: String(r.created_at),
1829
- updatedAt: String(r.updated_at)
1998
+ updatedAt: String(r.updated_at),
1999
+ checkpointCount: Number(r.checkpoint_count ?? 0),
2000
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
2001
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
2002
+ tokensUsed: Number(r.tokens_used ?? 0),
2003
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
1830
2004
  }));
1831
2005
  }
1832
2006
  function checkStaleCompletion(taskContext, taskCreatedAt) {
@@ -1834,8 +2008,13 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
1834
2008
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
1835
2009
  try {
1836
2010
  const since = new Date(taskCreatedAt).toISOString();
2011
+ const branch = execSync6(
2012
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
2013
+ { encoding: "utf8", timeout: 3e3 }
2014
+ ).trim();
2015
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
1837
2016
  const commitCount = execSync6(
1838
- `git log --oneline --since="${since}" 2>/dev/null | wc -l`,
2017
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
1839
2018
  { encoding: "utf8", timeout: 5e3 }
1840
2019
  ).trim();
1841
2020
  const count = parseInt(commitCount, 10);
@@ -1894,6 +2073,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1894
2073
  const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
1895
2074
  throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
1896
2075
  }
2076
+ try {
2077
+ await writeCheckpoint({
2078
+ taskId,
2079
+ step: "claimed",
2080
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
2081
+ });
2082
+ } catch {
2083
+ }
1897
2084
  return { row, taskFile, now, taskId };
1898
2085
  }
1899
2086
  if (input.result) {
@@ -1907,6 +2094,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
1907
2094
  args: [input.status, now, taskId]
1908
2095
  });
1909
2096
  }
2097
+ try {
2098
+ await writeCheckpoint({
2099
+ taskId,
2100
+ step: `status_transition:${input.status}`,
2101
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
2102
+ });
2103
+ } catch {
2104
+ }
1910
2105
  return { row, taskFile, now, taskId };
1911
2106
  }
1912
2107
  async function deleteTaskCore(taskId, _baseDir) {
@@ -2060,23 +2255,38 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
2060
2255
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
2061
2256
  try {
2062
2257
  const client = getClient();
2063
- const fileName = taskFile.split("/").pop() ?? "";
2064
- const reviewPrefix = fileName.replace(".md", "");
2065
- const parts = reviewPrefix.split("-");
2066
- if (parts.length >= 3 && parts[0] === "review") {
2067
- const agent = parts[1];
2068
- const slug = parts.slice(2).join("-");
2069
- const originalTaskFile = `exe/${agent}/${slug}.md`;
2258
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2259
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
2260
+ if (parentId) {
2070
2261
  const result = await client.execute({
2071
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
2072
- args: [(/* @__PURE__ */ new Date()).toISOString(), originalTaskFile]
2262
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
2263
+ args: [now, parentId]
2073
2264
  });
2074
2265
  if (result.rowsAffected > 0) {
2075
2266
  process.stderr.write(
2076
- `[review-cleanup] Cascaded original task to done: ${originalTaskFile}
2267
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
2077
2268
  `
2078
2269
  );
2079
2270
  }
2271
+ } else {
2272
+ const fileName = taskFile.split("/").pop() ?? "";
2273
+ const reviewPrefix = fileName.replace(".md", "");
2274
+ const parts = reviewPrefix.split("-");
2275
+ if (parts.length >= 3 && parts[0] === "review") {
2276
+ const agent = parts[1];
2277
+ const slug = parts.slice(2).join("-");
2278
+ const originalTaskFile = `exe/${agent}/${slug}.md`;
2279
+ const result = await client.execute({
2280
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
2281
+ args: [now, originalTaskFile]
2282
+ });
2283
+ if (result.rowsAffected > 0) {
2284
+ process.stderr.write(
2285
+ `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
2286
+ `
2287
+ );
2288
+ }
2289
+ }
2080
2290
  }
2081
2291
  } catch (err) {
2082
2292
  process.stderr.write(
@@ -2197,12 +2407,23 @@ function getProjectName(cwd) {
2197
2407
  const dir = cwd ?? process.cwd();
2198
2408
  if (_cached2 && _cachedCwd === dir) return _cached2;
2199
2409
  try {
2200
- const repoRoot = execSync7("git rev-parse --show-toplevel", {
2201
- cwd: dir,
2202
- encoding: "utf8",
2203
- timeout: 2e3,
2204
- stdio: ["pipe", "pipe", "pipe"]
2205
- }).trim();
2410
+ let repoRoot;
2411
+ try {
2412
+ const gitCommonDir = execSync7("git rev-parse --path-format=absolute --git-common-dir", {
2413
+ cwd: dir,
2414
+ encoding: "utf8",
2415
+ timeout: 2e3,
2416
+ stdio: ["pipe", "pipe", "pipe"]
2417
+ }).trim();
2418
+ repoRoot = path13.dirname(gitCommonDir);
2419
+ } catch {
2420
+ repoRoot = execSync7("git rev-parse --show-toplevel", {
2421
+ cwd: dir,
2422
+ encoding: "utf8",
2423
+ timeout: 2e3,
2424
+ stdio: ["pipe", "pipe", "pipe"]
2425
+ }).trim();
2426
+ }
2206
2427
  _cached2 = path13.basename(repoRoot);
2207
2428
  _cachedCwd = dir;
2208
2429
  return _cached2;
@@ -2308,7 +2529,9 @@ async function dispatchTaskToEmployee(input) {
2308
2529
  return { dispatched, session: sessionName, crossProject };
2309
2530
  } else {
2310
2531
  const projectDir = input.projectDir ?? process.cwd();
2311
- const result = ensureEmployee(input.assignedTo, exeSession, projectDir);
2532
+ const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
2533
+ autoInstance: input.assignedTo === "tom" || input.assignedTo === "sasha"
2534
+ });
2312
2535
  if (result.status === "failed") {
2313
2536
  process.stderr.write(
2314
2537
  `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
@@ -2734,7 +2957,8 @@ __export(tasks_exports, {
2734
2957
  resolveTask: () => resolveTask,
2735
2958
  slugify: () => slugify,
2736
2959
  updateTask: () => updateTask,
2737
- updateTaskStatus: () => updateTaskStatus
2960
+ updateTaskStatus: () => updateTaskStatus,
2961
+ writeCheckpoint: () => writeCheckpoint
2738
2962
  });
2739
2963
  import path14 from "path";
2740
2964
  import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3 } from "fs";
@@ -2776,10 +3000,11 @@ async function updateTask(input) {
2776
3000
  try {
2777
3001
  const client = getClient();
2778
3002
  const taskTitle = String(row.title);
3003
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
2779
3004
  await client.execute({
2780
3005
  sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
2781
- WHERE title LIKE ? AND status IN ('open', 'in_progress')`,
2782
- args: [now, `%left%${taskTitle}%in_progress`]
3006
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
3007
+ args: [now, `%left '${escaped}' as in\\_progress%`]
2783
3008
  });
2784
3009
  } catch {
2785
3010
  }
@@ -2837,6 +3062,10 @@ async function updateTask(input) {
2837
3062
  taskFile,
2838
3063
  createdAt: String(row.created_at),
2839
3064
  updatedAt: now,
3065
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
3066
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
3067
+ tokensUsed: Number(row.tokens_used ?? 0),
3068
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
2840
3069
  nextTask
2841
3070
  };
2842
3071
  }
@@ -3350,6 +3579,11 @@ function getSessionState(sessionName) {
3350
3579
  if (!transport.isAlive(sessionName)) return "offline";
3351
3580
  try {
3352
3581
  const pane = transport.capturePane(sessionName, 5);
3582
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
3583
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
3584
+ return "no_claude";
3585
+ }
3586
+ }
3353
3587
  if (/Running…/.test(pane)) return "tool";
3354
3588
  if (BUSY_PATTERN.test(pane)) return "thinking";
3355
3589
  return "idle";
@@ -3380,7 +3614,14 @@ function sendIntercom(targetSession) {
3380
3614
  logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
3381
3615
  return "failed";
3382
3616
  }
3383
- if (isSessionBusy(targetSession)) {
3617
+ const sessionState = getSessionState(targetSession);
3618
+ if (sessionState === "no_claude") {
3619
+ queueIntercom(targetSession, "claude not running in session");
3620
+ recordDebounce(targetSession);
3621
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
3622
+ return "queued";
3623
+ }
3624
+ if (sessionState === "thinking" || sessionState === "tool") {
3384
3625
  queueIntercom(targetSession, "session busy at send time");
3385
3626
  recordDebounce(targetSession);
3386
3627
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
@@ -3392,18 +3633,7 @@ function sendIntercom(targetSession) {
3392
3633
  }
3393
3634
  transport.sendKeys(targetSession, "/exe-intercom");
3394
3635
  recordDebounce(targetSession);
3395
- for (let i = 0; i < INTERCOM_POLL_MAX_ATTEMPTS; i++) {
3396
- try {
3397
- execSync8(`sleep ${INTERCOM_POLL_INTERVAL_S}`);
3398
- } catch {
3399
- }
3400
- const state = getSessionState(targetSession);
3401
- if (state === "thinking" || state === "tool") {
3402
- logIntercom(`ACKNOWLEDGED \u2192 ${targetSession} (state=${state}, poll=${i + 1})`);
3403
- return "acknowledged";
3404
- }
3405
- }
3406
- logIntercom(`DELIVERED \u2192 ${targetSession} (no state transition after ${INTERCOM_POLL_MAX_ATTEMPTS}s)`);
3636
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
3407
3637
  return "delivered";
3408
3638
  } catch {
3409
3639
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -3420,7 +3650,17 @@ function notifyParentExe(sessionKey) {
3420
3650
  process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
3421
3651
  `);
3422
3652
  const result = sendIntercom(target);
3423
- return result !== "failed";
3653
+ if (result === "failed") {
3654
+ const rootExe = resolveExeSession();
3655
+ if (rootExe && rootExe !== target) {
3656
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
3657
+ `);
3658
+ const fallback = sendIntercom(rootExe);
3659
+ return fallback !== "failed";
3660
+ }
3661
+ return false;
3662
+ }
3663
+ return true;
3424
3664
  }
3425
3665
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3426
3666
  if (employeeName === "exe") {
@@ -3469,7 +3709,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
3469
3709
  return { status: "failed", sessionName, error: "intercom delivery failed" };
3470
3710
  }
3471
3711
  const spawnOpts = { ...opts, instance: effectiveInstance };
3472
- const wtPath = ensureWorktree(projectDir, employeeName, effectiveInstance);
3712
+ const mainRoot = getMainRepoRoot(projectDir) ?? projectDir;
3713
+ const wtPath = ensureWorktree(mainRoot, employeeName, effectiveInstance);
3473
3714
  if (wtPath) {
3474
3715
  spawnOpts.cwd = wtPath;
3475
3716
  }
@@ -3650,7 +3891,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3650
3891
  let booted = false;
3651
3892
  for (let i = 0; i < 30; i++) {
3652
3893
  try {
3653
- execSync8("sleep 1");
3894
+ execSync8("sleep 0.5");
3654
3895
  } catch {
3655
3896
  }
3656
3897
  try {
@@ -3670,7 +3911,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3670
3911
  }
3671
3912
  }
3672
3913
  if (!booted) {
3673
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 30s` };
3914
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3674
3915
  }
3675
3916
  if (!useExeAgent) {
3676
3917
  try {
@@ -3688,7 +3929,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3688
3929
  });
3689
3930
  return { sessionName };
3690
3931
  }
3691
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN, INTERCOM_POLL_INTERVAL_S, INTERCOM_POLL_MAX_ATTEMPTS;
3932
+ var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3692
3933
  var init_tmux_routing = __esm({
3693
3934
  "src/lib/tmux-routing.ts"() {
3694
3935
  "use strict";
@@ -3709,8 +3950,6 @@ var init_tmux_routing = __esm({
3709
3950
  DEBOUNCE_FILE = path15.join(SESSION_CACHE, "intercom-debounce.json");
3710
3951
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3711
3952
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
3712
- INTERCOM_POLL_INTERVAL_S = 1;
3713
- INTERCOM_POLL_MAX_ATTEMPTS = 8;
3714
3953
  }
3715
3954
  });
3716
3955
 
@@ -3890,13 +4129,27 @@ async function ensureShardSchema(client) {
3890
4129
  "ALTER TABLE memories ADD COLUMN document_id TEXT",
3891
4130
  "ALTER TABLE memories ADD COLUMN user_id TEXT",
3892
4131
  "ALTER TABLE memories ADD COLUMN char_offset INTEGER",
3893
- "ALTER TABLE memories ADD COLUMN page_number INTEGER"
4132
+ "ALTER TABLE memories ADD COLUMN page_number INTEGER",
4133
+ // Source provenance columns (must match database.ts)
4134
+ "ALTER TABLE memories ADD COLUMN source_path TEXT",
4135
+ "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
4136
+ "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
4137
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
3894
4138
  ]) {
3895
4139
  try {
3896
4140
  await client.execute(col);
3897
4141
  } catch {
3898
4142
  }
3899
4143
  }
4144
+ for (const idx of [
4145
+ "CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)",
4146
+ "CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL"
4147
+ ]) {
4148
+ try {
4149
+ await client.execute(idx);
4150
+ } catch {
4151
+ }
4152
+ }
3900
4153
  try {
3901
4154
  await client.execute("CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status)");
3902
4155
  } catch {
@@ -4005,8 +4258,11 @@ var store_exports = {};
4005
4258
  __export(store_exports, {
4006
4259
  attachDocumentMetadata: () => attachDocumentMetadata,
4007
4260
  buildWikiScopeFilter: () => buildWikiScopeFilter,
4261
+ classifyTier: () => classifyTier,
4008
4262
  disposeStore: () => disposeStore,
4009
4263
  flushBatch: () => flushBatch,
4264
+ flushTier3: () => flushTier3,
4265
+ getMemoryCardinality: () => getMemoryCardinality,
4010
4266
  initStore: () => initStore,
4011
4267
  reserveVersions: () => reserveVersions,
4012
4268
  searchMemories: () => searchMemories,
@@ -4052,6 +4308,11 @@ async function initStore(options) {
4052
4308
  const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
4053
4309
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
4054
4310
  }
4311
+ function classifyTier(record) {
4312
+ if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
4313
+ if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
4314
+ return 3;
4315
+ }
4055
4316
  async function writeMemory(record) {
4056
4317
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
4057
4318
  throw new Error(
@@ -4079,7 +4340,11 @@ async function writeMemory(record) {
4079
4340
  document_id: record.document_id ?? null,
4080
4341
  user_id: record.user_id ?? null,
4081
4342
  char_offset: record.char_offset ?? null,
4082
- page_number: record.page_number ?? null
4343
+ page_number: record.page_number ?? null,
4344
+ source_path: record.source_path ?? null,
4345
+ source_type: record.source_type ?? null,
4346
+ tier: record.tier ?? classifyTier(record),
4347
+ supersedes_id: record.supersedes_id ?? null
4083
4348
  };
4084
4349
  _pendingRecords.push(dbRow);
4085
4350
  if (_flushTimer === null) {
@@ -4111,20 +4376,26 @@ async function flushBatch() {
4111
4376
  const userId = row.user_id ?? null;
4112
4377
  const charOffset = row.char_offset ?? null;
4113
4378
  const pageNumber = row.page_number ?? null;
4379
+ const sourcePath = row.source_path ?? null;
4380
+ const sourceType = row.source_type ?? null;
4381
+ const tier = row.tier ?? 3;
4382
+ const supersedesId = row.supersedes_id ?? null;
4114
4383
  return {
4115
4384
  sql: hasVector ? `INSERT OR IGNORE INTO memories
4116
4385
  (id, agent_id, agent_role, session_id, timestamp,
4117
4386
  tool_name, project_name,
4118
4387
  has_error, raw_text, vector, version, task_id, importance, status,
4119
4388
  confidence, last_accessed,
4120
- workspace_id, document_id, user_id, char_offset, page_number)
4121
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
4389
+ workspace_id, document_id, user_id, char_offset, page_number,
4390
+ source_path, source_type, tier, supersedes_id)
4391
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
4122
4392
  (id, agent_id, agent_role, session_id, timestamp,
4123
4393
  tool_name, project_name,
4124
4394
  has_error, raw_text, vector, version, task_id, importance, status,
4125
4395
  confidence, last_accessed,
4126
- workspace_id, document_id, user_id, char_offset, page_number)
4127
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4396
+ workspace_id, document_id, user_id, char_offset, page_number,
4397
+ source_path, source_type, tier, supersedes_id)
4398
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4128
4399
  args: hasVector ? [
4129
4400
  row.id,
4130
4401
  row.agent_id,
@@ -4146,7 +4417,11 @@ async function flushBatch() {
4146
4417
  documentId,
4147
4418
  userId,
4148
4419
  charOffset,
4149
- pageNumber
4420
+ pageNumber,
4421
+ sourcePath,
4422
+ sourceType,
4423
+ tier,
4424
+ supersedesId
4150
4425
  ] : [
4151
4426
  row.id,
4152
4427
  row.agent_id,
@@ -4167,7 +4442,11 @@ async function flushBatch() {
4167
4442
  documentId,
4168
4443
  userId,
4169
4444
  charOffset,
4170
- pageNumber
4445
+ pageNumber,
4446
+ sourcePath,
4447
+ sourceType,
4448
+ tier,
4449
+ supersedesId
4171
4450
  ]
4172
4451
  };
4173
4452
  };
@@ -4241,7 +4520,8 @@ async function searchMemories(queryVector, agentId, options) {
4241
4520
  has_error, raw_text, vector, importance, status,
4242
4521
  confidence, last_accessed,
4243
4522
  workspace_id, document_id, user_id,
4244
- char_offset, page_number
4523
+ char_offset, page_number,
4524
+ source_path, source_type
4245
4525
  FROM memories
4246
4526
  WHERE agent_id = ?
4247
4527
  AND vector IS NOT NULL${statusFilter}
@@ -4290,7 +4570,9 @@ async function searchMemories(queryVector, agentId, options) {
4290
4570
  document_id: row.document_id ?? null,
4291
4571
  user_id: row.user_id ?? null,
4292
4572
  char_offset: row.char_offset ?? null,
4293
- page_number: row.page_number ?? null
4573
+ page_number: row.page_number ?? null,
4574
+ source_path: row.source_path ?? null,
4575
+ source_type: row.source_type ?? null
4294
4576
  }));
4295
4577
  }
4296
4578
  async function attachDocumentMetadata(records) {
@@ -4328,6 +4610,25 @@ async function attachDocumentMetadata(records) {
4328
4610
  }
4329
4611
  return records;
4330
4612
  }
4613
+ async function flushTier3(agentId, options) {
4614
+ const client = getClient();
4615
+ const maxAge = options?.maxAgeHours ?? 72;
4616
+ const cutoff = new Date(Date.now() - maxAge * 36e5).toISOString();
4617
+ if (options?.dryRun) {
4618
+ const result2 = await client.execute({
4619
+ sql: `SELECT COUNT(*) as cnt FROM memories
4620
+ WHERE agent_id = ? AND tier = 3 AND status = 'active' AND timestamp < ?`,
4621
+ args: [agentId, cutoff]
4622
+ });
4623
+ return { archived: Number(result2.rows[0]?.cnt ?? 0) };
4624
+ }
4625
+ const result = await client.execute({
4626
+ sql: `UPDATE memories SET status = 'archived'
4627
+ WHERE agent_id = ? AND tier = 3 AND status = 'active' AND timestamp < ?`,
4628
+ args: [agentId, cutoff]
4629
+ });
4630
+ return { archived: result.rowsAffected };
4631
+ }
4331
4632
  async function disposeStore() {
4332
4633
  if (_flushTimer !== null) {
4333
4634
  clearInterval(_flushTimer);
@@ -4358,6 +4659,18 @@ function reserveVersions(count) {
4358
4659
  }
4359
4660
  return reserved;
4360
4661
  }
4662
+ async function getMemoryCardinality(agentId) {
4663
+ try {
4664
+ const client = getClient();
4665
+ const result = await client.execute({
4666
+ sql: `SELECT COUNT(*) as cnt FROM memories WHERE agent_id = ? AND COALESCE(status, 'active') = 'active'`,
4667
+ args: [agentId]
4668
+ });
4669
+ return Number(result.rows[0]?.cnt) || 0;
4670
+ } catch {
4671
+ return 0;
4672
+ }
4673
+ }
4361
4674
  var _pendingRecords, _batchSize, _flushIntervalMs, _flushTimer, _flushing, _nextVersion;
4362
4675
  var init_store = __esm({
4363
4676
  "src/lib/store.ts"() {
@@ -6462,6 +6775,25 @@ async function* agentLoop(userMessage, history, config2) {
6462
6775
  }
6463
6776
  totalUsage.inputTokens += response.usage.inputTokens;
6464
6777
  totalUsage.outputTokens += response.usage.outputTokens;
6778
+ if (config2.tokenBudgetMiddleware) {
6779
+ const result = await config2.tokenBudgetMiddleware.onTokenUsed(
6780
+ response.usage.inputTokens,
6781
+ response.usage.outputTokens
6782
+ );
6783
+ if (result.warned && config2.hooks.onNotification) {
6784
+ await config2.hooks.onNotification(
6785
+ `\u26A0\uFE0F Token budget at ${result.percentUsed}%. Fallback model: ${result.fallback ?? "none (task will terminate at 100%)"}.`
6786
+ );
6787
+ }
6788
+ if (result.exceeded) {
6789
+ if (config2.hooks.onTokenBudgetExceeded) {
6790
+ await config2.hooks.onTokenBudgetExceeded(context, result.fallback);
6791
+ }
6792
+ abortController.abort();
6793
+ yield { type: "error", error: new Error("Token budget exceeded. Task requires manual continuation.") };
6794
+ break;
6795
+ }
6796
+ }
6465
6797
  contextManager.updateFromApiUsage(response.usage.inputTokens, response.usage.outputTokens);
6466
6798
  contextManager.updateFromMessages(messages);
6467
6799
  await contextManager.checkPressure();
@@ -7007,12 +7339,58 @@ function composeHooks(...pipelines) {
7007
7339
  for (const p of pipelines) {
7008
7340
  if (p.onCrossAgentMessage) await p.onCrossAgentMessage(event);
7009
7341
  }
7342
+ },
7343
+ async onTokenBudgetExceeded(ctx, fallback) {
7344
+ for (const p of pipelines) {
7345
+ if (p.onTokenBudgetExceeded) await p.onTokenBudgetExceeded(ctx, fallback);
7346
+ }
7010
7347
  }
7011
7348
  };
7012
7349
  }
7013
7350
 
7014
7351
  // src/lib/task-router.ts
7015
7352
  import { randomUUID } from "crypto";
7353
+ var DEFAULT_BLOOM_CONFIG = {
7354
+ complexityToTier: {
7355
+ routine: "junior",
7356
+ standard: "standard",
7357
+ complex: "senior",
7358
+ critical: "specialist"
7359
+ },
7360
+ tierRules: {
7361
+ junior: {
7362
+ eligible: ["tom"],
7363
+ reviewRequired: false,
7364
+ manualOnly: false
7365
+ },
7366
+ standard: {
7367
+ eligible: ["tom"],
7368
+ reviewRequired: false,
7369
+ manualOnly: false
7370
+ },
7371
+ senior: {
7372
+ eligible: [],
7373
+ // any specialist, but review required
7374
+ reviewRequired: true,
7375
+ manualOnly: false
7376
+ },
7377
+ specialist: {
7378
+ eligible: [],
7379
+ reviewRequired: true,
7380
+ manualOnly: true
7381
+ }
7382
+ }
7383
+ };
7384
+ function resolveBloomRouting(complexity, config2 = DEFAULT_BLOOM_CONFIG) {
7385
+ const tier = config2.complexityToTier[complexity];
7386
+ const rule = config2.tierRules[tier];
7387
+ return {
7388
+ tier,
7389
+ reviewRequired: rule.reviewRequired,
7390
+ manualOnly: rule.manualOnly,
7391
+ eligible: rule.eligible
7392
+ };
7393
+ }
7016
7394
  async function scoreEmployee(taskVector, agentId, searchFn) {
7017
7395
  const results = await searchFn(taskVector, agentId, { limit: 5 });
7018
7396
  if (results.length === 0) {
@@ -7025,13 +7403,29 @@ async function scoreEmployee(taskVector, agentId, searchFn) {
7025
7403
  }
7026
7404
  return { agentId, score: results.length / 5 };
7027
7405
  }
7028
- async function routeTask(taskDescription, employees, embedFn, searchFn) {
7029
- const specialists = employees.filter((e) => e.name !== "exe");
7406
+ async function routeTask(taskDescription, employees, embedFn, searchFn, options) {
7407
+ let specialists = employees.filter((e) => e.name !== "exe");
7030
7408
  if (specialists.length === 0) {
7031
7409
  throw new Error(
7032
7410
  "No specialist employees available. Create one with /exe-new-employee."
7033
7411
  );
7034
7412
  }
7413
+ let bloomRouting;
7414
+ if (options?.complexity) {
7415
+ bloomRouting = resolveBloomRouting(options.complexity, options.bloomConfig);
7416
+ if (bloomRouting.manualOnly) {
7417
+ throw new Error(
7418
+ `Task complexity "${options.complexity}" requires manual assignment (tier: ${bloomRouting.tier}).`
7419
+ );
7420
+ }
7421
+ if (bloomRouting.eligible.length > 0) {
7422
+ const eligible = new Set(bloomRouting.eligible);
7423
+ const filtered = specialists.filter((e) => eligible.has(e.name));
7424
+ if (filtered.length > 0) {
7425
+ specialists = filtered;
7426
+ }
7427
+ }
7428
+ }
7035
7429
  const taskVector = await embedFn(taskDescription);
7036
7430
  const scored = await Promise.all(
7037
7431
  specialists.map(async (emp) => {
@@ -7040,7 +7434,7 @@ async function routeTask(taskDescription, employees, embedFn, searchFn) {
7040
7434
  })
7041
7435
  );
7042
7436
  scored.sort((a, b) => b.score - a.score);
7043
- return scored[0];
7437
+ return { ...scored[0], bloomRouting };
7044
7438
  }
7045
7439
 
7046
7440
  // src/runtime/orchestrator.ts
@@ -11715,7 +12109,92 @@ async function executeCreateTask(params) {
11715
12109
  baseDir: process.cwd()
11716
12110
  });
11717
12111
  }
11718
- async function executeAction(action, record, executor) {
12112
+ async function executeUpdateWiki(params) {
12113
+ const apiUrl = process.env.EXE_WIKI_API_URL;
12114
+ const apiKey = process.env.EXE_WIKI_API_KEY;
12115
+ if (!apiUrl || !apiKey) {
12116
+ throw new Error("Wiki not configured: EXE_WIKI_API_URL / EXE_WIKI_API_KEY not set");
12117
+ }
12118
+ const workspace = params.workspace;
12119
+ const content = params.content ?? params.text;
12120
+ const mode = params.mode ?? "append";
12121
+ const section = params.section;
12122
+ if (!workspace || !content) {
12123
+ throw new Error("update_wiki requires 'workspace' and 'content' params");
12124
+ }
12125
+ const documentId = params.document_id;
12126
+ if (documentId && mode === "append") {
12127
+ const readRes = await fetch(`${apiUrl}/v1/document/${documentId}`, {
12128
+ headers: { Authorization: `Bearer ${apiKey}` },
12129
+ signal: AbortSignal.timeout(15e3)
12130
+ });
12131
+ if (!readRes.ok) {
12132
+ throw new Error(`Wiki read failed (${readRes.status})`);
12133
+ }
12134
+ const doc = await readRes.json();
12135
+ const existingContent = String(doc.content ?? "");
12136
+ const title = String(doc.title ?? "Untitled");
12137
+ await fetch(`${apiUrl}/v1/document/${documentId}`, {
12138
+ method: "DELETE",
12139
+ headers: { Authorization: `Bearer ${apiKey}` },
12140
+ signal: AbortSignal.timeout(15e3)
12141
+ }).catch(() => {
12142
+ });
12143
+ const uploadRes = await fetch(`${apiUrl}/v1/document/raw-text`, {
12144
+ method: "POST",
12145
+ headers: {
12146
+ "Content-Type": "application/json",
12147
+ Authorization: `Bearer ${apiKey}`
12148
+ },
12149
+ body: JSON.stringify({
12150
+ textContent: section ? existingContent + `
12151
+
12152
+ ## ${section}
12153
+ ${content}` : existingContent + "\n\n" + content,
12154
+ metadata: { title },
12155
+ workspaceSlugs: [workspace]
12156
+ }),
12157
+ signal: AbortSignal.timeout(15e3)
12158
+ });
12159
+ if (!uploadRes.ok) throw new Error(`Wiki upload failed (${uploadRes.status})`);
12160
+ } else {
12161
+ const title = params.title ?? "Auto-generated";
12162
+ const res = await fetch(`${apiUrl}/v1/document/raw-text`, {
12163
+ method: "POST",
12164
+ headers: {
12165
+ "Content-Type": "application/json",
12166
+ Authorization: `Bearer ${apiKey}`
12167
+ },
12168
+ body: JSON.stringify({
12169
+ textContent: content,
12170
+ metadata: { title },
12171
+ workspaceSlugs: [workspace]
12172
+ }),
12173
+ signal: AbortSignal.timeout(15e3)
12174
+ });
12175
+ if (!res.ok) throw new Error(`Wiki create failed (${res.status})`);
12176
+ }
12177
+ }
12178
+ async function routeToApproval(action, resolvedParams, triggerName) {
12179
+ const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
12180
+ 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)}`;
12181
+ await createTask2({
12182
+ title: `[Approval Required] ${triggerName}: ${action.type}`,
12183
+ assignedTo: "exe",
12184
+ assignedBy: "trigger-engine",
12185
+ projectName: resolvedParams.project ?? "exe-os",
12186
+ priority: "p1",
12187
+ context: `Trigger "${triggerName}" wants to execute this action but requires approval.
12188
+
12189
+ **Action:** ${action.type}
12190
+ **Summary:** ${actionSummary}
12191
+ **Full params:** ${JSON.stringify(resolvedParams, null, 2)}
12192
+
12193
+ To approve, manually run the action via MCP tools.`,
12194
+ baseDir: process.cwd()
12195
+ });
12196
+ }
12197
+ async function executeAction(action, record, executor, triggerName) {
11719
12198
  if (executor) {
11720
12199
  return executor(action, record);
11721
12200
  }
@@ -11723,6 +12202,17 @@ async function executeAction(action, record, executor) {
11723
12202
  for (const [key, val] of Object.entries(action.params)) {
11724
12203
  resolvedParams[key] = substituteTemplate(val, record);
11725
12204
  }
12205
+ if (action.requires_approval) {
12206
+ try {
12207
+ await routeToApproval(action, resolvedParams, triggerName ?? "Unknown trigger");
12208
+ return { success: true };
12209
+ } catch (err) {
12210
+ return {
12211
+ success: false,
12212
+ error: `Approval routing failed: ${err instanceof Error ? err.message : String(err)}`
12213
+ };
12214
+ }
12215
+ }
11726
12216
  try {
11727
12217
  switch (action.type) {
11728
12218
  case "send_whatsapp":
@@ -11734,6 +12224,9 @@ async function executeAction(action, record, executor) {
11734
12224
  case "create_task":
11735
12225
  await executeCreateTask(resolvedParams);
11736
12226
  break;
12227
+ case "update_wiki":
12228
+ await executeUpdateWiki(resolvedParams);
12229
+ break;
11737
12230
  case "mcp_tool":
11738
12231
  console.log(
11739
12232
  `[trigger-engine] mcp_tool action: ${JSON.stringify(resolvedParams)}`
@@ -11758,7 +12251,7 @@ async function processCRMEvent(event, executor, triggersOverride) {
11758
12251
  if (!evaluateConditions(trigger.conditions, event.record)) continue;
11759
12252
  const actionResults = [];
11760
12253
  for (const action of trigger.actions) {
11761
- const result = await executeAction(action, event.record, executor);
12254
+ const result = await executeAction(action, event.record, executor, trigger.name);
11762
12255
  actionResults.push({
11763
12256
  type: action.type,
11764
12257
  success: result.success,