@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
@@ -65,12 +65,23 @@ function getProjectName(cwd) {
65
65
  const dir = cwd ?? process.cwd();
66
66
  if (_cached && _cachedCwd === dir) return _cached;
67
67
  try {
68
- const repoRoot = execSync2("git rev-parse --show-toplevel", {
69
- cwd: dir,
70
- encoding: "utf8",
71
- timeout: 2e3,
72
- stdio: ["pipe", "pipe", "pipe"]
73
- }).trim();
68
+ let repoRoot;
69
+ try {
70
+ const gitCommonDir = execSync2("git rev-parse --path-format=absolute --git-common-dir", {
71
+ cwd: dir,
72
+ encoding: "utf8",
73
+ timeout: 2e3,
74
+ stdio: ["pipe", "pipe", "pipe"]
75
+ }).trim();
76
+ repoRoot = path2.dirname(gitCommonDir);
77
+ } catch {
78
+ repoRoot = execSync2("git rev-parse --show-toplevel", {
79
+ cwd: dir,
80
+ encoding: "utf8",
81
+ timeout: 2e3,
82
+ stdio: ["pipe", "pipe", "pipe"]
83
+ }).trim();
84
+ }
74
85
  _cached = path2.basename(repoRoot);
75
86
  _cachedCwd = dir;
76
87
  return _cached;
@@ -319,6 +330,27 @@ async function ensureSchema() {
319
330
  });
320
331
  } catch {
321
332
  }
333
+ try {
334
+ await client.execute({
335
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint TEXT`,
336
+ args: []
337
+ });
338
+ } catch {
339
+ }
340
+ try {
341
+ await client.execute({
342
+ sql: `ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER NOT NULL DEFAULT 0`,
343
+ args: []
344
+ });
345
+ } catch {
346
+ }
347
+ try {
348
+ await client.execute({
349
+ sql: `ALTER TABLE tasks ADD COLUMN complexity TEXT NOT NULL DEFAULT 'standard'`,
350
+ args: []
351
+ });
352
+ } catch {
353
+ }
322
354
  try {
323
355
  await client.execute({
324
356
  sql: `ALTER TABLE memories ADD COLUMN task_id TEXT`,
@@ -729,6 +761,15 @@ async function ensureSchema() {
729
761
  } catch {
730
762
  }
731
763
  }
764
+ for (const col of [
765
+ "ALTER TABLE memories ADD COLUMN source_path TEXT",
766
+ "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'"
767
+ ]) {
768
+ try {
769
+ await client.execute(col);
770
+ } catch {
771
+ }
772
+ }
732
773
  await client.executeMultiple(`
733
774
  CREATE INDEX IF NOT EXISTS idx_memories_workspace
734
775
  ON memories(workspace_id);
@@ -793,6 +834,34 @@ async function ensureSchema() {
793
834
  CREATE INDEX IF NOT EXISTS idx_conversations_channel
794
835
  ON conversations(channel_id);
795
836
  `);
837
+ try {
838
+ await client.execute({
839
+ sql: `ALTER TABLE tasks ADD COLUMN budget_tokens INTEGER`,
840
+ args: []
841
+ });
842
+ } catch {
843
+ }
844
+ try {
845
+ await client.execute({
846
+ sql: `ALTER TABLE tasks ADD COLUMN budget_fallback_model TEXT`,
847
+ args: []
848
+ });
849
+ } catch {
850
+ }
851
+ try {
852
+ await client.execute({
853
+ sql: `ALTER TABLE tasks ADD COLUMN tokens_used INTEGER DEFAULT 0`,
854
+ args: []
855
+ });
856
+ } catch {
857
+ }
858
+ try {
859
+ await client.execute({
860
+ sql: `ALTER TABLE tasks ADD COLUMN tokens_warned_at INTEGER`,
861
+ args: []
862
+ });
863
+ } catch {
864
+ }
796
865
  await client.executeMultiple(`
797
866
  CREATE VIRTUAL TABLE IF NOT EXISTS conversations_fts USING fts5(
798
867
  content_text,
@@ -819,6 +888,52 @@ async function ensureSchema() {
819
888
  VALUES (new.rowid, new.content_text, new.sender_name, new.agent_response);
820
889
  END;
821
890
  `);
891
+ try {
892
+ await client.execute({
893
+ sql: `ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3`,
894
+ args: []
895
+ });
896
+ } catch {
897
+ }
898
+ try {
899
+ await client.execute(
900
+ `CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)`
901
+ );
902
+ } catch {
903
+ }
904
+ try {
905
+ await client.execute({
906
+ sql: `UPDATE memories SET tier = 1 WHERE tool_name = 'commit_to_long_term_memory' AND importance >= 8 AND tier = 3`,
907
+ args: []
908
+ });
909
+ await client.execute({
910
+ sql: `UPDATE memories SET tier = 2 WHERE tool_name IN ('store_memory', 'manual') AND importance >= 5 AND tier = 3`,
911
+ args: []
912
+ });
913
+ } catch {
914
+ }
915
+ try {
916
+ await client.execute({
917
+ sql: `ALTER TABLE memories ADD COLUMN supersedes_id TEXT`,
918
+ args: []
919
+ });
920
+ } catch {
921
+ }
922
+ try {
923
+ await client.execute(
924
+ `CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL`
925
+ );
926
+ } catch {
927
+ }
928
+ for (const col of [
929
+ "ALTER TABLE tasks ADD COLUMN checkpoint TEXT",
930
+ "ALTER TABLE tasks ADD COLUMN checkpoint_count INTEGER DEFAULT 0"
931
+ ]) {
932
+ try {
933
+ await client.execute(col);
934
+ } catch {
935
+ }
936
+ }
822
937
  }
823
938
  async function disposeDatabase() {
824
939
  if (_client) {
@@ -917,6 +1032,11 @@ function normalizeSessionLifecycle(raw) {
917
1032
  const userSL = raw.sessionLifecycle ?? {};
918
1033
  raw.sessionLifecycle = { ...defaultSL, ...userSL };
919
1034
  }
1035
+ function normalizeAutoUpdate(raw) {
1036
+ const defaultAU = DEFAULT_CONFIG.autoUpdate;
1037
+ const userAU = raw.autoUpdate ?? {};
1038
+ raw.autoUpdate = { ...defaultAU, ...userAU };
1039
+ }
920
1040
  async function loadConfig() {
921
1041
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
922
1042
  await mkdir2(dir, { recursive: true });
@@ -939,6 +1059,7 @@ async function loadConfig() {
939
1059
  }
940
1060
  normalizeScalingRoadmap(migratedCfg);
941
1061
  normalizeSessionLifecycle(migratedCfg);
1062
+ normalizeAutoUpdate(migratedCfg);
942
1063
  const config = { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db"), ...migratedCfg };
943
1064
  if (config.dbPath.startsWith("~")) {
944
1065
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -961,6 +1082,7 @@ function loadConfigSync() {
961
1082
  const { config: migratedCfg } = migrateConfig(parsed);
962
1083
  normalizeScalingRoadmap(migratedCfg);
963
1084
  normalizeSessionLifecycle(migratedCfg);
1085
+ normalizeAutoUpdate(migratedCfg);
964
1086
  return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db"), ...migratedCfg };
965
1087
  } catch {
966
1088
  return { ...DEFAULT_CONFIG, dbPath: path4.join(dir, "memories.db") };
@@ -980,6 +1102,7 @@ async function loadConfigFrom(configPath) {
980
1102
  const { config: migratedCfg } = migrateConfig(parsed);
981
1103
  normalizeScalingRoadmap(migratedCfg);
982
1104
  normalizeSessionLifecycle(migratedCfg);
1105
+ normalizeAutoUpdate(migratedCfg);
983
1106
  return { ...DEFAULT_CONFIG, ...migratedCfg };
984
1107
  } catch {
985
1108
  return { ...DEFAULT_CONFIG };
@@ -1051,6 +1174,11 @@ var init_config = __esm({
1051
1174
  idleKillTicksRequired: 3,
1052
1175
  idleKillIntercomAckWindowMs: 1e4,
1053
1176
  maxAutoInstances: 10
1177
+ },
1178
+ autoUpdate: {
1179
+ checkOnBoot: true,
1180
+ autoInstall: false,
1181
+ checkIntervalMs: 24 * 60 * 60 * 1e3
1054
1182
  }
1055
1183
  };
1056
1184
  CONFIG_MIGRATIONS = [
@@ -1184,13 +1312,27 @@ async function ensureShardSchema(client) {
1184
1312
  "ALTER TABLE memories ADD COLUMN document_id TEXT",
1185
1313
  "ALTER TABLE memories ADD COLUMN user_id TEXT",
1186
1314
  "ALTER TABLE memories ADD COLUMN char_offset INTEGER",
1187
- "ALTER TABLE memories ADD COLUMN page_number INTEGER"
1315
+ "ALTER TABLE memories ADD COLUMN page_number INTEGER",
1316
+ // Source provenance columns (must match database.ts)
1317
+ "ALTER TABLE memories ADD COLUMN source_path TEXT",
1318
+ "ALTER TABLE memories ADD COLUMN source_type TEXT DEFAULT 'text'",
1319
+ "ALTER TABLE memories ADD COLUMN tier INTEGER DEFAULT 3",
1320
+ "ALTER TABLE memories ADD COLUMN supersedes_id TEXT"
1188
1321
  ]) {
1189
1322
  try {
1190
1323
  await client.execute(col);
1191
1324
  } catch {
1192
1325
  }
1193
1326
  }
1327
+ for (const idx of [
1328
+ "CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)",
1329
+ "CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories(supersedes_id) WHERE supersedes_id IS NOT NULL"
1330
+ ]) {
1331
+ try {
1332
+ await client.execute(idx);
1333
+ } catch {
1334
+ }
1335
+ }
1194
1336
  try {
1195
1337
  await client.execute("CREATE INDEX IF NOT EXISTS idx_memories_status ON memories(status)");
1196
1338
  } catch {
@@ -2021,6 +2163,36 @@ import path11 from "path";
2021
2163
  import { execSync as execSync4 } from "child_process";
2022
2164
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
2023
2165
  import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
2166
+ async function writeCheckpoint(input2) {
2167
+ const client = getClient();
2168
+ const row = await resolveTask(client, input2.taskId);
2169
+ const taskId = String(row.id);
2170
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2171
+ const blockedByIds = [];
2172
+ if (row.blocked_by) {
2173
+ blockedByIds.push(String(row.blocked_by));
2174
+ }
2175
+ const checkpoint = {
2176
+ step: input2.step,
2177
+ context_summary: input2.contextSummary,
2178
+ files_touched: input2.filesTouched ?? [],
2179
+ blocked_by_ids: blockedByIds,
2180
+ last_checkpoint_at: now
2181
+ };
2182
+ const result = await client.execute({
2183
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
2184
+ args: [JSON.stringify(checkpoint), now, taskId]
2185
+ });
2186
+ if (result.rowsAffected === 0) {
2187
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
2188
+ }
2189
+ const countResult = await client.execute({
2190
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
2191
+ args: [taskId]
2192
+ });
2193
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
2194
+ return { checkpointCount };
2195
+ }
2024
2196
  function extractParentFromContext(contextBody) {
2025
2197
  if (!contextBody) return null;
2026
2198
  const match = contextBody.match(
@@ -2127,9 +2299,10 @@ async function createTaskCore(input2) {
2127
2299
  } catch {
2128
2300
  }
2129
2301
  }
2302
+ const complexity = input2.complexity ?? "standard";
2130
2303
  await client.execute({
2131
- 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)
2132
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2304
+ 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)
2305
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2133
2306
  args: [
2134
2307
  id,
2135
2308
  input2.title,
@@ -2143,6 +2316,11 @@ async function createTaskCore(input2) {
2143
2316
  parentTaskId,
2144
2317
  input2.reviewer ?? null,
2145
2318
  input2.context,
2319
+ input2.budgetTokens ?? null,
2320
+ input2.budgetFallbackModel ?? null,
2321
+ 0,
2322
+ null,
2323
+ complexity,
2146
2324
  now,
2147
2325
  now
2148
2326
  ]
@@ -2158,7 +2336,11 @@ async function createTaskCore(input2) {
2158
2336
  taskFile,
2159
2337
  createdAt: now,
2160
2338
  updatedAt: now,
2161
- warning
2339
+ warning,
2340
+ budgetTokens: input2.budgetTokens ?? null,
2341
+ budgetFallbackModel: input2.budgetFallbackModel ?? null,
2342
+ tokensUsed: 0,
2343
+ tokensWarnedAt: null
2162
2344
  };
2163
2345
  }
2164
2346
  async function listTasks(input2) {
@@ -2198,7 +2380,12 @@ async function listTasks(input2) {
2198
2380
  status: String(r.status),
2199
2381
  taskFile: String(r.task_file),
2200
2382
  createdAt: String(r.created_at),
2201
- updatedAt: String(r.updated_at)
2383
+ updatedAt: String(r.updated_at),
2384
+ checkpointCount: Number(r.checkpoint_count ?? 0),
2385
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
2386
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
2387
+ tokensUsed: Number(r.tokens_used ?? 0),
2388
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
2202
2389
  }));
2203
2390
  }
2204
2391
  function checkStaleCompletion(taskContext, taskCreatedAt) {
@@ -2206,8 +2393,13 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
2206
2393
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
2207
2394
  try {
2208
2395
  const since = new Date(taskCreatedAt).toISOString();
2396
+ const branch = execSync4(
2397
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
2398
+ { encoding: "utf8", timeout: 3e3 }
2399
+ ).trim();
2400
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
2209
2401
  const commitCount = execSync4(
2210
- `git log --oneline --since="${since}" 2>/dev/null | wc -l`,
2402
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
2211
2403
  { encoding: "utf8", timeout: 5e3 }
2212
2404
  ).trim();
2213
2405
  const count = parseInt(commitCount, 10);
@@ -2266,6 +2458,14 @@ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
2266
2458
  const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
2267
2459
  throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
2268
2460
  }
2461
+ try {
2462
+ await writeCheckpoint({
2463
+ taskId,
2464
+ step: "claimed",
2465
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
2466
+ });
2467
+ } catch {
2468
+ }
2269
2469
  return { row, taskFile, now, taskId };
2270
2470
  }
2271
2471
  if (input2.result) {
@@ -2279,6 +2479,14 @@ ${input2.result}` : `\u26A0\uFE0F ${warning}`;
2279
2479
  args: [input2.status, now, taskId]
2280
2480
  });
2281
2481
  }
2482
+ try {
2483
+ await writeCheckpoint({
2484
+ taskId,
2485
+ step: `status_transition:${input2.status}`,
2486
+ contextSummary: input2.result ? `Transitioned to ${input2.status}. Result: ${input2.result.slice(0, 500)}` : `Transitioned to ${input2.status}.`
2487
+ });
2488
+ } catch {
2489
+ }
2282
2490
  return { row, taskFile, now, taskId };
2283
2491
  }
2284
2492
  async function deleteTaskCore(taskId, _baseDir) {
@@ -2615,11 +2823,12 @@ function queueIntercom(targetSession, reason) {
2615
2823
  }
2616
2824
  writeQueue(queue);
2617
2825
  }
2618
- var QUEUE_PATH, INTERCOM_LOG;
2826
+ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
2619
2827
  var init_intercom_queue = __esm({
2620
2828
  "src/lib/intercom-queue.ts"() {
2621
2829
  "use strict";
2622
2830
  QUEUE_PATH = path13.join(os4.homedir(), ".exe-os", "intercom-queue.json");
2831
+ TTL_MS = 60 * 60 * 1e3;
2623
2832
  INTERCOM_LOG = path13.join(os4.homedir(), ".exe-os", "intercom.log");
2624
2833
  }
2625
2834
  });
@@ -2641,6 +2850,17 @@ function getGitRoot(dir) {
2641
2850
  return null;
2642
2851
  }
2643
2852
  }
2853
+ function getMainRepoRoot(dir) {
2854
+ try {
2855
+ const commonDir = execSync7(
2856
+ "git rev-parse --path-format=absolute --git-common-dir",
2857
+ { cwd: dir, encoding: "utf-8", timeout: GIT_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"] }
2858
+ ).trim();
2859
+ return realpath(path14.dirname(commonDir));
2860
+ } catch {
2861
+ return null;
2862
+ }
2863
+ }
2644
2864
  function worktreePath(repoRoot, employeeName, instance) {
2645
2865
  const label = instanceLabel(employeeName, instance);
2646
2866
  return path14.join(repoRoot, ".worktrees", label);
@@ -2877,6 +3097,11 @@ function getSessionState(sessionName) {
2877
3097
  if (!transport.isAlive(sessionName)) return "offline";
2878
3098
  try {
2879
3099
  const pane = transport.capturePane(sessionName, 5);
3100
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
3101
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
3102
+ return "no_claude";
3103
+ }
3104
+ }
2880
3105
  if (/Running…/.test(pane)) return "tool";
2881
3106
  if (BUSY_PATTERN.test(pane)) return "thinking";
2882
3107
  return "idle";
@@ -2884,10 +3109,6 @@ function getSessionState(sessionName) {
2884
3109
  return "offline";
2885
3110
  }
2886
3111
  }
2887
- function isSessionBusy(sessionName) {
2888
- const state = getSessionState(sessionName);
2889
- return state === "thinking" || state === "tool";
2890
- }
2891
3112
  function isExeSession(sessionName) {
2892
3113
  return /^exe\d*$/.test(sessionName);
2893
3114
  }
@@ -2907,7 +3128,14 @@ function sendIntercom(targetSession) {
2907
3128
  logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
2908
3129
  return "failed";
2909
3130
  }
2910
- if (isSessionBusy(targetSession)) {
3131
+ const sessionState = getSessionState(targetSession);
3132
+ if (sessionState === "no_claude") {
3133
+ queueIntercom(targetSession, "claude not running in session");
3134
+ recordDebounce(targetSession);
3135
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
3136
+ return "queued";
3137
+ }
3138
+ if (sessionState === "thinking" || sessionState === "tool") {
2911
3139
  queueIntercom(targetSession, "session busy at send time");
2912
3140
  recordDebounce(targetSession);
2913
3141
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
@@ -2919,18 +3147,7 @@ function sendIntercom(targetSession) {
2919
3147
  }
2920
3148
  transport.sendKeys(targetSession, "/exe-intercom");
2921
3149
  recordDebounce(targetSession);
2922
- for (let i = 0; i < INTERCOM_POLL_MAX_ATTEMPTS; i++) {
2923
- try {
2924
- execSync8(`sleep ${INTERCOM_POLL_INTERVAL_S}`);
2925
- } catch {
2926
- }
2927
- const state = getSessionState(targetSession);
2928
- if (state === "thinking" || state === "tool") {
2929
- logIntercom(`ACKNOWLEDGED \u2192 ${targetSession} (state=${state}, poll=${i + 1})`);
2930
- return "acknowledged";
2931
- }
2932
- }
2933
- logIntercom(`DELIVERED \u2192 ${targetSession} (no state transition after ${INTERCOM_POLL_MAX_ATTEMPTS}s)`);
3150
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
2934
3151
  return "delivered";
2935
3152
  } catch {
2936
3153
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -2947,7 +3164,17 @@ function notifyParentExe(sessionKey) {
2947
3164
  process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
2948
3165
  `);
2949
3166
  const result = sendIntercom(target);
2950
- return result !== "failed";
3167
+ if (result === "failed") {
3168
+ const rootExe = resolveExeSession();
3169
+ if (rootExe && rootExe !== target) {
3170
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
3171
+ `);
3172
+ const fallback = sendIntercom(rootExe);
3173
+ return fallback !== "failed";
3174
+ }
3175
+ return false;
3176
+ }
3177
+ return true;
2951
3178
  }
2952
3179
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2953
3180
  if (employeeName === "exe") {
@@ -2996,7 +3223,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
2996
3223
  return { status: "failed", sessionName, error: "intercom delivery failed" };
2997
3224
  }
2998
3225
  const spawnOpts = { ...opts, instance: effectiveInstance };
2999
- const wtPath = ensureWorktree(projectDir, employeeName, effectiveInstance);
3226
+ const mainRoot = getMainRepoRoot(projectDir) ?? projectDir;
3227
+ const wtPath = ensureWorktree(mainRoot, employeeName, effectiveInstance);
3000
3228
  if (wtPath) {
3001
3229
  spawnOpts.cwd = wtPath;
3002
3230
  }
@@ -3177,7 +3405,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3177
3405
  let booted = false;
3178
3406
  for (let i = 0; i < 30; i++) {
3179
3407
  try {
3180
- execSync8("sleep 1");
3408
+ execSync8("sleep 0.5");
3181
3409
  } catch {
3182
3410
  }
3183
3411
  try {
@@ -3197,7 +3425,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3197
3425
  }
3198
3426
  }
3199
3427
  if (!booted) {
3200
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 30s` };
3428
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
3201
3429
  }
3202
3430
  if (!useExeAgent) {
3203
3431
  try {
@@ -3215,7 +3443,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
3215
3443
  });
3216
3444
  return { sessionName };
3217
3445
  }
3218
- 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;
3446
+ var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
3219
3447
  var init_tmux_routing = __esm({
3220
3448
  "src/lib/tmux-routing.ts"() {
3221
3449
  "use strict";
@@ -3235,8 +3463,6 @@ var init_tmux_routing = __esm({
3235
3463
  DEBOUNCE_FILE = path15.join(SESSION_CACHE, "intercom-debounce.json");
3236
3464
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
3237
3465
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
3238
- INTERCOM_POLL_INTERVAL_S = 1;
3239
- INTERCOM_POLL_MAX_ATTEMPTS = 8;
3240
3466
  }
3241
3467
  });
3242
3468
 
@@ -3322,23 +3548,38 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
3322
3548
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
3323
3549
  try {
3324
3550
  const client = getClient();
3325
- const fileName = taskFile.split("/").pop() ?? "";
3326
- const reviewPrefix = fileName.replace(".md", "");
3327
- const parts = reviewPrefix.split("-");
3328
- if (parts.length >= 3 && parts[0] === "review") {
3329
- const agent = parts[1];
3330
- const slug = parts.slice(2).join("-");
3331
- const originalTaskFile = `exe/${agent}/${slug}.md`;
3551
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3552
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
3553
+ if (parentId) {
3332
3554
  const result = await client.execute({
3333
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
3334
- args: [(/* @__PURE__ */ new Date()).toISOString(), originalTaskFile]
3555
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
3556
+ args: [now, parentId]
3335
3557
  });
3336
3558
  if (result.rowsAffected > 0) {
3337
3559
  process.stderr.write(
3338
- `[review-cleanup] Cascaded original task to done: ${originalTaskFile}
3560
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
3339
3561
  `
3340
3562
  );
3341
3563
  }
3564
+ } else {
3565
+ const fileName = taskFile.split("/").pop() ?? "";
3566
+ const reviewPrefix = fileName.replace(".md", "");
3567
+ const parts = reviewPrefix.split("-");
3568
+ if (parts.length >= 3 && parts[0] === "review") {
3569
+ const agent = parts[1];
3570
+ const slug = parts.slice(2).join("-");
3571
+ const originalTaskFile = `exe/${agent}/${slug}.md`;
3572
+ const result = await client.execute({
3573
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
3574
+ args: [now, originalTaskFile]
3575
+ });
3576
+ if (result.rowsAffected > 0) {
3577
+ process.stderr.write(
3578
+ `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
3579
+ `
3580
+ );
3581
+ }
3582
+ }
3342
3583
  }
3343
3584
  } catch (err) {
3344
3585
  process.stderr.write(
@@ -3539,7 +3780,9 @@ async function dispatchTaskToEmployee(input2) {
3539
3780
  return { dispatched, session: sessionName, crossProject };
3540
3781
  } else {
3541
3782
  const projectDir = input2.projectDir ?? process.cwd();
3542
- const result = ensureEmployee(input2.assignedTo, exeSession, projectDir);
3783
+ const result = ensureEmployee(input2.assignedTo, exeSession, projectDir, {
3784
+ autoInstance: input2.assignedTo === "tom" || input2.assignedTo === "sasha"
3785
+ });
3543
3786
  if (result.status === "failed") {
3544
3787
  process.stderr.write(
3545
3788
  `[dispatch] Failed to spawn ${input2.assignedTo}: ${result.error}
@@ -3903,7 +4146,8 @@ __export(tasks_exports, {
3903
4146
  resolveTask: () => resolveTask,
3904
4147
  slugify: () => slugify,
3905
4148
  updateTask: () => updateTask,
3906
- updateTaskStatus: () => updateTaskStatus
4149
+ updateTaskStatus: () => updateTaskStatus,
4150
+ writeCheckpoint: () => writeCheckpoint
3907
4151
  });
3908
4152
  import path18 from "path";
3909
4153
  import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4 } from "fs";
@@ -3945,10 +4189,11 @@ async function updateTask(input2) {
3945
4189
  try {
3946
4190
  const client = getClient();
3947
4191
  const taskTitle = String(row.title);
4192
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
3948
4193
  await client.execute({
3949
4194
  sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3950
- WHERE title LIKE ? AND status IN ('open', 'in_progress')`,
3951
- args: [now, `%left%${taskTitle}%in_progress`]
4195
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
4196
+ args: [now, `%left '${escaped}' as in\\_progress%`]
3952
4197
  });
3953
4198
  } catch {
3954
4199
  }
@@ -4006,6 +4251,10 @@ async function updateTask(input2) {
4006
4251
  taskFile,
4007
4252
  createdAt: String(row.created_at),
4008
4253
  updatedAt: now,
4254
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
4255
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
4256
+ tokensUsed: Number(row.tokens_used ?? 0),
4257
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
4009
4258
  nextTask
4010
4259
  };
4011
4260
  }
@@ -4240,6 +4489,11 @@ async function initStore(options) {
4240
4489
  const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
4241
4490
  _nextVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
4242
4491
  }
4492
+ function classifyTier(record) {
4493
+ if (record.tool_name === "commit_to_long_term_memory" && (record.importance ?? 0) >= 8) return 1;
4494
+ if (["store_memory", "manual"].includes(record.tool_name ?? "") && (record.importance ?? 0) >= 5) return 2;
4495
+ return 3;
4496
+ }
4243
4497
  async function writeMemory(record) {
4244
4498
  if (record.vector !== null && record.vector.length !== EMBEDDING_DIM) {
4245
4499
  throw new Error(
@@ -4267,7 +4521,11 @@ async function writeMemory(record) {
4267
4521
  document_id: record.document_id ?? null,
4268
4522
  user_id: record.user_id ?? null,
4269
4523
  char_offset: record.char_offset ?? null,
4270
- page_number: record.page_number ?? null
4524
+ page_number: record.page_number ?? null,
4525
+ source_path: record.source_path ?? null,
4526
+ source_type: record.source_type ?? null,
4527
+ tier: record.tier ?? classifyTier(record),
4528
+ supersedes_id: record.supersedes_id ?? null
4271
4529
  };
4272
4530
  _pendingRecords.push(dbRow);
4273
4531
  if (_flushTimer === null) {
@@ -4299,20 +4557,26 @@ async function flushBatch() {
4299
4557
  const userId = row.user_id ?? null;
4300
4558
  const charOffset = row.char_offset ?? null;
4301
4559
  const pageNumber = row.page_number ?? null;
4560
+ const sourcePath = row.source_path ?? null;
4561
+ const sourceType = row.source_type ?? null;
4562
+ const tier = row.tier ?? 3;
4563
+ const supersedesId = row.supersedes_id ?? null;
4302
4564
  return {
4303
4565
  sql: hasVector ? `INSERT OR IGNORE INTO memories
4304
4566
  (id, agent_id, agent_role, session_id, timestamp,
4305
4567
  tool_name, project_name,
4306
4568
  has_error, raw_text, vector, version, task_id, importance, status,
4307
4569
  confidence, last_accessed,
4308
- workspace_id, document_id, user_id, char_offset, page_number)
4309
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
4570
+ workspace_id, document_id, user_id, char_offset, page_number,
4571
+ source_path, source_type, tier, supersedes_id)
4572
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` : `INSERT OR IGNORE INTO memories
4310
4573
  (id, agent_id, agent_role, session_id, timestamp,
4311
4574
  tool_name, project_name,
4312
4575
  has_error, raw_text, vector, version, task_id, importance, status,
4313
4576
  confidence, last_accessed,
4314
- workspace_id, document_id, user_id, char_offset, page_number)
4315
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4577
+ workspace_id, document_id, user_id, char_offset, page_number,
4578
+ source_path, source_type, tier, supersedes_id)
4579
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
4316
4580
  args: hasVector ? [
4317
4581
  row.id,
4318
4582
  row.agent_id,
@@ -4334,7 +4598,11 @@ async function flushBatch() {
4334
4598
  documentId,
4335
4599
  userId,
4336
4600
  charOffset,
4337
- pageNumber
4601
+ pageNumber,
4602
+ sourcePath,
4603
+ sourceType,
4604
+ tier,
4605
+ supersedesId
4338
4606
  ] : [
4339
4607
  row.id,
4340
4608
  row.agent_id,
@@ -4355,7 +4623,11 @@ async function flushBatch() {
4355
4623
  documentId,
4356
4624
  userId,
4357
4625
  charOffset,
4358
- pageNumber
4626
+ pageNumber,
4627
+ sourcePath,
4628
+ sourceType,
4629
+ tier,
4630
+ supersedesId
4359
4631
  ]
4360
4632
  };
4361
4633
  };