@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/lib/tasks.js CHANGED
@@ -101,6 +101,11 @@ function normalizeSessionLifecycle(raw) {
101
101
  const userSL = raw.sessionLifecycle ?? {};
102
102
  raw.sessionLifecycle = { ...defaultSL, ...userSL };
103
103
  }
104
+ function normalizeAutoUpdate(raw) {
105
+ const defaultAU = DEFAULT_CONFIG.autoUpdate;
106
+ const userAU = raw.autoUpdate ?? {};
107
+ raw.autoUpdate = { ...defaultAU, ...userAU };
108
+ }
104
109
  async function loadConfig() {
105
110
  const dir = process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? EXE_AI_DIR;
106
111
  await mkdir(dir, { recursive: true });
@@ -123,6 +128,7 @@ async function loadConfig() {
123
128
  }
124
129
  normalizeScalingRoadmap(migratedCfg);
125
130
  normalizeSessionLifecycle(migratedCfg);
131
+ normalizeAutoUpdate(migratedCfg);
126
132
  const config = { ...DEFAULT_CONFIG, dbPath: path.join(dir, "memories.db"), ...migratedCfg };
127
133
  if (config.dbPath.startsWith("~")) {
128
134
  config.dbPath = config.dbPath.replace(/^~/, os.homedir());
@@ -198,6 +204,11 @@ var init_config = __esm({
198
204
  idleKillTicksRequired: 3,
199
205
  idleKillIntercomAckWindowMs: 1e4,
200
206
  maxAutoInstances: 10
207
+ },
208
+ autoUpdate: {
209
+ checkOnBoot: true,
210
+ autoInstall: false,
211
+ checkIntervalMs: 24 * 60 * 60 * 1e3
201
212
  }
202
213
  };
203
214
  CONFIG_MIGRATIONS = [
@@ -271,6 +282,36 @@ import path3 from "path";
271
282
  import { execSync } from "child_process";
272
283
  import { mkdir as mkdir2, writeFile as writeFile2, appendFile } from "fs/promises";
273
284
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
285
+ async function writeCheckpoint(input) {
286
+ const client = getClient();
287
+ const row = await resolveTask(client, input.taskId);
288
+ const taskId = String(row.id);
289
+ const now = (/* @__PURE__ */ new Date()).toISOString();
290
+ const blockedByIds = [];
291
+ if (row.blocked_by) {
292
+ blockedByIds.push(String(row.blocked_by));
293
+ }
294
+ const checkpoint = {
295
+ step: input.step,
296
+ context_summary: input.contextSummary,
297
+ files_touched: input.filesTouched ?? [],
298
+ blocked_by_ids: blockedByIds,
299
+ last_checkpoint_at: now
300
+ };
301
+ const result = await client.execute({
302
+ sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
303
+ args: [JSON.stringify(checkpoint), now, taskId]
304
+ });
305
+ if (result.rowsAffected === 0) {
306
+ throw new Error(`Checkpoint write failed: task ${taskId} not found`);
307
+ }
308
+ const countResult = await client.execute({
309
+ sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
310
+ args: [taskId]
311
+ });
312
+ const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
313
+ return { checkpointCount };
314
+ }
274
315
  function extractParentFromContext(contextBody) {
275
316
  if (!contextBody) return null;
276
317
  const match = contextBody.match(
@@ -377,9 +418,10 @@ async function createTaskCore(input) {
377
418
  } catch {
378
419
  }
379
420
  }
421
+ const complexity = input.complexity ?? "standard";
380
422
  await client.execute({
381
- 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)
382
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
423
+ 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)
424
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
383
425
  args: [
384
426
  id,
385
427
  input.title,
@@ -393,6 +435,11 @@ async function createTaskCore(input) {
393
435
  parentTaskId,
394
436
  input.reviewer ?? null,
395
437
  input.context,
438
+ input.budgetTokens ?? null,
439
+ input.budgetFallbackModel ?? null,
440
+ 0,
441
+ null,
442
+ complexity,
396
443
  now,
397
444
  now
398
445
  ]
@@ -408,7 +455,11 @@ async function createTaskCore(input) {
408
455
  taskFile,
409
456
  createdAt: now,
410
457
  updatedAt: now,
411
- warning
458
+ warning,
459
+ budgetTokens: input.budgetTokens ?? null,
460
+ budgetFallbackModel: input.budgetFallbackModel ?? null,
461
+ tokensUsed: 0,
462
+ tokensWarnedAt: null
412
463
  };
413
464
  }
414
465
  async function listTasks(input) {
@@ -448,7 +499,12 @@ async function listTasks(input) {
448
499
  status: String(r.status),
449
500
  taskFile: String(r.task_file),
450
501
  createdAt: String(r.created_at),
451
- updatedAt: String(r.updated_at)
502
+ updatedAt: String(r.updated_at),
503
+ checkpointCount: Number(r.checkpoint_count ?? 0),
504
+ budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
505
+ budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
506
+ tokensUsed: Number(r.tokens_used ?? 0),
507
+ tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
452
508
  }));
453
509
  }
454
510
  function checkStaleCompletion(taskContext, taskCreatedAt) {
@@ -456,8 +512,13 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
456
512
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
457
513
  try {
458
514
  const since = new Date(taskCreatedAt).toISOString();
515
+ const branch = execSync(
516
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
517
+ { encoding: "utf8", timeout: 3e3 }
518
+ ).trim();
519
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
459
520
  const commitCount = execSync(
460
- `git log --oneline --since="${since}" 2>/dev/null | wc -l`,
521
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
461
522
  { encoding: "utf8", timeout: 5e3 }
462
523
  ).trim();
463
524
  const count = parseInt(commitCount, 10);
@@ -516,6 +577,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
516
577
  const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
517
578
  throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
518
579
  }
580
+ try {
581
+ await writeCheckpoint({
582
+ taskId,
583
+ step: "claimed",
584
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
585
+ });
586
+ } catch {
587
+ }
519
588
  return { row, taskFile, now, taskId };
520
589
  }
521
590
  if (input.result) {
@@ -529,6 +598,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
529
598
  args: [input.status, now, taskId]
530
599
  });
531
600
  }
601
+ try {
602
+ await writeCheckpoint({
603
+ taskId,
604
+ step: `status_transition:${input.status}`,
605
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
606
+ });
607
+ } catch {
608
+ }
532
609
  return { row, taskFile, now, taskId };
533
610
  }
534
611
  async function deleteTaskCore(taskId, _baseDir) {
@@ -902,11 +979,12 @@ function queueIntercom(targetSession, reason) {
902
979
  }
903
980
  writeQueue(queue);
904
981
  }
905
- var QUEUE_PATH, INTERCOM_LOG;
982
+ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
906
983
  var init_intercom_queue = __esm({
907
984
  "src/lib/intercom-queue.ts"() {
908
985
  "use strict";
909
986
  QUEUE_PATH = path6.join(os4.homedir(), ".exe-os", "intercom-queue.json");
987
+ TTL_MS = 60 * 60 * 1e3;
910
988
  INTERCOM_LOG = path6.join(os4.homedir(), ".exe-os", "intercom.log");
911
989
  }
912
990
  });
@@ -1029,6 +1107,17 @@ function getGitRoot(dir) {
1029
1107
  return null;
1030
1108
  }
1031
1109
  }
1110
+ function getMainRepoRoot(dir) {
1111
+ try {
1112
+ const commonDir = execSync5(
1113
+ "git rev-parse --path-format=absolute --git-common-dir",
1114
+ { cwd: dir, encoding: "utf-8", timeout: GIT_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"] }
1115
+ ).trim();
1116
+ return realpath(path9.dirname(commonDir));
1117
+ } catch {
1118
+ return null;
1119
+ }
1120
+ }
1032
1121
  function worktreePath(repoRoot, employeeName, instance) {
1033
1122
  const label = instanceLabel(employeeName, instance);
1034
1123
  return path9.join(repoRoot, ".worktrees", label);
@@ -1265,6 +1354,11 @@ function getSessionState(sessionName) {
1265
1354
  if (!transport.isAlive(sessionName)) return "offline";
1266
1355
  try {
1267
1356
  const pane = transport.capturePane(sessionName, 5);
1357
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
1358
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
1359
+ return "no_claude";
1360
+ }
1361
+ }
1268
1362
  if (/Running…/.test(pane)) return "tool";
1269
1363
  if (BUSY_PATTERN.test(pane)) return "thinking";
1270
1364
  return "idle";
@@ -1272,10 +1366,6 @@ function getSessionState(sessionName) {
1272
1366
  return "offline";
1273
1367
  }
1274
1368
  }
1275
- function isSessionBusy(sessionName) {
1276
- const state = getSessionState(sessionName);
1277
- return state === "thinking" || state === "tool";
1278
- }
1279
1369
  function isExeSession(sessionName) {
1280
1370
  return /^exe\d*$/.test(sessionName);
1281
1371
  }
@@ -1295,7 +1385,14 @@ function sendIntercom(targetSession) {
1295
1385
  logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
1296
1386
  return "failed";
1297
1387
  }
1298
- if (isSessionBusy(targetSession)) {
1388
+ const sessionState = getSessionState(targetSession);
1389
+ if (sessionState === "no_claude") {
1390
+ queueIntercom(targetSession, "claude not running in session");
1391
+ recordDebounce(targetSession);
1392
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
1393
+ return "queued";
1394
+ }
1395
+ if (sessionState === "thinking" || sessionState === "tool") {
1299
1396
  queueIntercom(targetSession, "session busy at send time");
1300
1397
  recordDebounce(targetSession);
1301
1398
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
@@ -1307,18 +1404,7 @@ function sendIntercom(targetSession) {
1307
1404
  }
1308
1405
  transport.sendKeys(targetSession, "/exe-intercom");
1309
1406
  recordDebounce(targetSession);
1310
- for (let i = 0; i < INTERCOM_POLL_MAX_ATTEMPTS; i++) {
1311
- try {
1312
- execSync6(`sleep ${INTERCOM_POLL_INTERVAL_S}`);
1313
- } catch {
1314
- }
1315
- const state = getSessionState(targetSession);
1316
- if (state === "thinking" || state === "tool") {
1317
- logIntercom(`ACKNOWLEDGED \u2192 ${targetSession} (state=${state}, poll=${i + 1})`);
1318
- return "acknowledged";
1319
- }
1320
- }
1321
- logIntercom(`DELIVERED \u2192 ${targetSession} (no state transition after ${INTERCOM_POLL_MAX_ATTEMPTS}s)`);
1407
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
1322
1408
  return "delivered";
1323
1409
  } catch {
1324
1410
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -1335,7 +1421,17 @@ function notifyParentExe(sessionKey) {
1335
1421
  process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
1336
1422
  `);
1337
1423
  const result = sendIntercom(target);
1338
- return result !== "failed";
1424
+ if (result === "failed") {
1425
+ const rootExe = resolveExeSession();
1426
+ if (rootExe && rootExe !== target) {
1427
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
1428
+ `);
1429
+ const fallback = sendIntercom(rootExe);
1430
+ return fallback !== "failed";
1431
+ }
1432
+ return false;
1433
+ }
1434
+ return true;
1339
1435
  }
1340
1436
  function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1341
1437
  if (employeeName === "exe") {
@@ -1384,7 +1480,8 @@ function ensureEmployee(employeeName, exeSession, projectDir, opts) {
1384
1480
  return { status: "failed", sessionName, error: "intercom delivery failed" };
1385
1481
  }
1386
1482
  const spawnOpts = { ...opts, instance: effectiveInstance };
1387
- const wtPath = ensureWorktree(projectDir, employeeName, effectiveInstance);
1483
+ const mainRoot = getMainRepoRoot(projectDir) ?? projectDir;
1484
+ const wtPath = ensureWorktree(mainRoot, employeeName, effectiveInstance);
1388
1485
  if (wtPath) {
1389
1486
  spawnOpts.cwd = wtPath;
1390
1487
  }
@@ -1565,7 +1662,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1565
1662
  let booted = false;
1566
1663
  for (let i = 0; i < 30; i++) {
1567
1664
  try {
1568
- execSync6("sleep 1");
1665
+ execSync6("sleep 0.5");
1569
1666
  } catch {
1570
1667
  }
1571
1668
  try {
@@ -1585,7 +1682,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1585
1682
  }
1586
1683
  }
1587
1684
  if (!booted) {
1588
- return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 30s` };
1685
+ return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
1589
1686
  }
1590
1687
  if (!useExeAgent) {
1591
1688
  try {
@@ -1603,7 +1700,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
1603
1700
  });
1604
1701
  return { sessionName };
1605
1702
  }
1606
- 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;
1703
+ var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
1607
1704
  var init_tmux_routing = __esm({
1608
1705
  "src/lib/tmux-routing.ts"() {
1609
1706
  "use strict";
@@ -1623,8 +1720,6 @@ var init_tmux_routing = __esm({
1623
1720
  DEBOUNCE_FILE = path10.join(SESSION_CACHE, "intercom-debounce.json");
1624
1721
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
1625
1722
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
1626
- INTERCOM_POLL_INTERVAL_S = 1;
1627
- INTERCOM_POLL_MAX_ATTEMPTS = 8;
1628
1723
  }
1629
1724
  });
1630
1725
 
@@ -1710,23 +1805,38 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
1710
1805
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
1711
1806
  try {
1712
1807
  const client = getClient();
1713
- const fileName = taskFile.split("/").pop() ?? "";
1714
- const reviewPrefix = fileName.replace(".md", "");
1715
- const parts = reviewPrefix.split("-");
1716
- if (parts.length >= 3 && parts[0] === "review") {
1717
- const agent = parts[1];
1718
- const slug = parts.slice(2).join("-");
1719
- const originalTaskFile = `exe/${agent}/${slug}.md`;
1808
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1809
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
1810
+ if (parentId) {
1720
1811
  const result = await client.execute({
1721
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
1722
- args: [(/* @__PURE__ */ new Date()).toISOString(), originalTaskFile]
1812
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
1813
+ args: [now, parentId]
1723
1814
  });
1724
1815
  if (result.rowsAffected > 0) {
1725
1816
  process.stderr.write(
1726
- `[review-cleanup] Cascaded original task to done: ${originalTaskFile}
1817
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
1727
1818
  `
1728
1819
  );
1729
1820
  }
1821
+ } else {
1822
+ const fileName = taskFile.split("/").pop() ?? "";
1823
+ const reviewPrefix = fileName.replace(".md", "");
1824
+ const parts = reviewPrefix.split("-");
1825
+ if (parts.length >= 3 && parts[0] === "review") {
1826
+ const agent = parts[1];
1827
+ const slug = parts.slice(2).join("-");
1828
+ const originalTaskFile = `exe/${agent}/${slug}.md`;
1829
+ const result = await client.execute({
1830
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
1831
+ args: [now, originalTaskFile]
1832
+ });
1833
+ if (result.rowsAffected > 0) {
1834
+ process.stderr.write(
1835
+ `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
1836
+ `
1837
+ );
1838
+ }
1839
+ }
1730
1840
  }
1731
1841
  } catch (err) {
1732
1842
  process.stderr.write(
@@ -1847,12 +1957,23 @@ function getProjectName(cwd) {
1847
1957
  const dir = cwd ?? process.cwd();
1848
1958
  if (_cached2 && _cachedCwd === dir) return _cached2;
1849
1959
  try {
1850
- const repoRoot = execSync7("git rev-parse --show-toplevel", {
1851
- cwd: dir,
1852
- encoding: "utf8",
1853
- timeout: 2e3,
1854
- stdio: ["pipe", "pipe", "pipe"]
1855
- }).trim();
1960
+ let repoRoot;
1961
+ try {
1962
+ const gitCommonDir = execSync7("git rev-parse --path-format=absolute --git-common-dir", {
1963
+ cwd: dir,
1964
+ encoding: "utf8",
1965
+ timeout: 2e3,
1966
+ stdio: ["pipe", "pipe", "pipe"]
1967
+ }).trim();
1968
+ repoRoot = path13.dirname(gitCommonDir);
1969
+ } catch {
1970
+ repoRoot = execSync7("git rev-parse --show-toplevel", {
1971
+ cwd: dir,
1972
+ encoding: "utf8",
1973
+ timeout: 2e3,
1974
+ stdio: ["pipe", "pipe", "pipe"]
1975
+ }).trim();
1976
+ }
1856
1977
  _cached2 = path13.basename(repoRoot);
1857
1978
  _cachedCwd = dir;
1858
1979
  return _cached2;
@@ -1958,7 +2079,9 @@ async function dispatchTaskToEmployee(input) {
1958
2079
  return { dispatched, session: sessionName, crossProject };
1959
2080
  } else {
1960
2081
  const projectDir = input.projectDir ?? process.cwd();
1961
- const result = ensureEmployee(input.assignedTo, exeSession, projectDir);
2082
+ const result = ensureEmployee(input.assignedTo, exeSession, projectDir, {
2083
+ autoInstance: input.assignedTo === "tom" || input.assignedTo === "sasha"
2084
+ });
1962
2085
  if (result.status === "failed") {
1963
2086
  process.stderr.write(
1964
2087
  `[dispatch] Failed to spawn ${input.assignedTo}: ${result.error}
@@ -2346,10 +2469,11 @@ async function updateTask(input) {
2346
2469
  try {
2347
2470
  const client = getClient();
2348
2471
  const taskTitle = String(row.title);
2472
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
2349
2473
  await client.execute({
2350
2474
  sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
2351
- WHERE title LIKE ? AND status IN ('open', 'in_progress')`,
2352
- args: [now, `%left%${taskTitle}%in_progress`]
2475
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
2476
+ args: [now, `%left '${escaped}' as in\\_progress%`]
2353
2477
  });
2354
2478
  } catch {
2355
2479
  }
@@ -2407,6 +2531,10 @@ async function updateTask(input) {
2407
2531
  taskFile,
2408
2532
  createdAt: String(row.created_at),
2409
2533
  updatedAt: now,
2534
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
2535
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
2536
+ tokensUsed: Number(row.tokens_used ?? 0),
2537
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
2410
2538
  nextTask
2411
2539
  };
2412
2540
  }
@@ -2452,5 +2580,6 @@ export {
2452
2580
  resolveTask,
2453
2581
  slugify,
2454
2582
  updateTask,
2455
- updateTaskStatus
2583
+ updateTaskStatus,
2584
+ writeCheckpoint
2456
2585
  };