@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
@@ -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
  async function resolveTask(client, identifier) {
275
316
  let result = await client.execute({
276
317
  sql: "SELECT * FROM tasks WHERE id = ?",
@@ -319,8 +360,13 @@ function checkStaleCompletion(taskContext, taskCreatedAt) {
319
360
  if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
320
361
  try {
321
362
  const since = new Date(taskCreatedAt).toISOString();
363
+ const branch = execSync(
364
+ "git rev-parse --abbrev-ref HEAD 2>/dev/null",
365
+ { encoding: "utf8", timeout: 3e3 }
366
+ ).trim();
367
+ const branchArg = branch && branch !== "HEAD" ? branch : "";
322
368
  const commitCount = execSync(
323
- `git log --oneline --since="${since}" 2>/dev/null | wc -l`,
369
+ `git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
324
370
  { encoding: "utf8", timeout: 5e3 }
325
371
  ).trim();
326
372
  const count = parseInt(commitCount, 10);
@@ -379,6 +425,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
379
425
  const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
380
426
  throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
381
427
  }
428
+ try {
429
+ await writeCheckpoint({
430
+ taskId,
431
+ step: "claimed",
432
+ contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
433
+ });
434
+ } catch {
435
+ }
382
436
  return { row, taskFile, now, taskId };
383
437
  }
384
438
  if (input.result) {
@@ -392,6 +446,14 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
392
446
  args: [input.status, now, taskId]
393
447
  });
394
448
  }
449
+ try {
450
+ await writeCheckpoint({
451
+ taskId,
452
+ step: `status_transition:${input.status}`,
453
+ contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
454
+ });
455
+ } catch {
456
+ }
395
457
  return { row, taskFile, now, taskId };
396
458
  }
397
459
  var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
@@ -639,11 +701,12 @@ function queueIntercom(targetSession, reason) {
639
701
  }
640
702
  writeQueue(queue);
641
703
  }
642
- var QUEUE_PATH, INTERCOM_LOG;
704
+ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
643
705
  var init_intercom_queue = __esm({
644
706
  "src/lib/intercom-queue.ts"() {
645
707
  "use strict";
646
708
  QUEUE_PATH = path6.join(os4.homedir(), ".exe-os", "intercom-queue.json");
709
+ TTL_MS = 60 * 60 * 1e3;
647
710
  INTERCOM_LOG = path6.join(os4.homedir(), ".exe-os", "intercom.log");
648
711
  }
649
712
  });
@@ -687,11 +750,25 @@ var init_worktree = __esm({
687
750
  });
688
751
 
689
752
  // src/lib/tmux-routing.ts
690
- import { execFileSync as execFileSync2, execSync as execSync5 } from "child_process";
691
753
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync8, appendFileSync } from "fs";
692
754
  import path9 from "path";
693
755
  import os5 from "os";
694
756
  import { fileURLToPath } from "url";
757
+ function getMySession() {
758
+ return getTransport().getMySession();
759
+ }
760
+ function extractRootExe(name) {
761
+ const match = name.match(/(exe\d+)$/);
762
+ return match?.[1] ?? null;
763
+ }
764
+ function getParentExe(sessionKey) {
765
+ try {
766
+ const data = JSON.parse(readFileSync7(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
767
+ return data.parentExe || null;
768
+ } catch {
769
+ return null;
770
+ }
771
+ }
695
772
  function getDispatchedBy(sessionKey) {
696
773
  try {
697
774
  const data = JSON.parse(readFileSync7(
@@ -703,6 +780,19 @@ function getDispatchedBy(sessionKey) {
703
780
  return null;
704
781
  }
705
782
  }
783
+ function resolveExeSession() {
784
+ const mySession = getMySession();
785
+ if (!mySession) return null;
786
+ try {
787
+ const key = getSessionKey();
788
+ const parentExe = getParentExe(key);
789
+ if (parentExe) {
790
+ return extractRootExe(parentExe) ?? parentExe;
791
+ }
792
+ } catch {
793
+ }
794
+ return extractRootExe(mySession) ?? mySession;
795
+ }
706
796
  function readDebounceState() {
707
797
  try {
708
798
  if (!existsSync8(DEBOUNCE_FILE)) return {};
@@ -747,6 +837,11 @@ function getSessionState(sessionName) {
747
837
  if (!transport.isAlive(sessionName)) return "offline";
748
838
  try {
749
839
  const pane = transport.capturePane(sessionName, 5);
840
+ if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
841
+ if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
842
+ return "no_claude";
843
+ }
844
+ }
750
845
  if (/Running…/.test(pane)) return "tool";
751
846
  if (BUSY_PATTERN.test(pane)) return "thinking";
752
847
  return "idle";
@@ -754,10 +849,6 @@ function getSessionState(sessionName) {
754
849
  return "offline";
755
850
  }
756
851
  }
757
- function isSessionBusy(sessionName) {
758
- const state = getSessionState(sessionName);
759
- return state === "thinking" || state === "tool";
760
- }
761
852
  function isExeSession(sessionName) {
762
853
  return /^exe\d*$/.test(sessionName);
763
854
  }
@@ -777,7 +868,14 @@ function sendIntercom(targetSession) {
777
868
  logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
778
869
  return "failed";
779
870
  }
780
- if (isSessionBusy(targetSession)) {
871
+ const sessionState = getSessionState(targetSession);
872
+ if (sessionState === "no_claude") {
873
+ queueIntercom(targetSession, "claude not running in session");
874
+ recordDebounce(targetSession);
875
+ logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
876
+ return "queued";
877
+ }
878
+ if (sessionState === "thinking" || sessionState === "tool") {
781
879
  queueIntercom(targetSession, "session busy at send time");
782
880
  recordDebounce(targetSession);
783
881
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
@@ -789,18 +887,7 @@ function sendIntercom(targetSession) {
789
887
  }
790
888
  transport.sendKeys(targetSession, "/exe-intercom");
791
889
  recordDebounce(targetSession);
792
- for (let i = 0; i < INTERCOM_POLL_MAX_ATTEMPTS; i++) {
793
- try {
794
- execSync5(`sleep ${INTERCOM_POLL_INTERVAL_S}`);
795
- } catch {
796
- }
797
- const state = getSessionState(targetSession);
798
- if (state === "thinking" || state === "tool") {
799
- logIntercom(`ACKNOWLEDGED \u2192 ${targetSession} (state=${state}, poll=${i + 1})`);
800
- return "acknowledged";
801
- }
802
- }
803
- logIntercom(`DELIVERED \u2192 ${targetSession} (no state transition after ${INTERCOM_POLL_MAX_ATTEMPTS}s)`);
890
+ logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
804
891
  return "delivered";
805
892
  } catch {
806
893
  logIntercom(`FAIL \u2192 ${targetSession}`);
@@ -817,9 +904,19 @@ function notifyParentExe(sessionKey) {
817
904
  process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
818
905
  `);
819
906
  const result = sendIntercom(target);
820
- return result !== "failed";
907
+ if (result === "failed") {
908
+ const rootExe = resolveExeSession();
909
+ if (rootExe && rootExe !== target) {
910
+ process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
911
+ `);
912
+ const fallback = sendIntercom(rootExe);
913
+ return fallback !== "failed";
914
+ }
915
+ return false;
916
+ }
917
+ return true;
821
918
  }
822
- var SESSION_CACHE, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN, INTERCOM_POLL_INTERVAL_S, INTERCOM_POLL_MAX_ATTEMPTS;
919
+ var SESSION_CACHE, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
823
920
  var init_tmux_routing = __esm({
824
921
  "src/lib/tmux-routing.ts"() {
825
922
  "use strict";
@@ -838,8 +935,6 @@ var init_tmux_routing = __esm({
838
935
  DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
839
936
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
840
937
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
841
- INTERCOM_POLL_INTERVAL_S = 1;
842
- INTERCOM_POLL_MAX_ATTEMPTS = 8;
843
938
  }
844
939
  });
845
940
 
@@ -850,23 +945,38 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
850
945
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
851
946
  try {
852
947
  const client = getClient();
853
- const fileName = taskFile.split("/").pop() ?? "";
854
- const reviewPrefix = fileName.replace(".md", "");
855
- const parts = reviewPrefix.split("-");
856
- if (parts.length >= 3 && parts[0] === "review") {
857
- const agent = parts[1];
858
- const slug = parts.slice(2).join("-");
859
- const originalTaskFile = `exe/${agent}/${slug}.md`;
948
+ const now = (/* @__PURE__ */ new Date()).toISOString();
949
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
950
+ if (parentId) {
860
951
  const result = await client.execute({
861
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
862
- args: [(/* @__PURE__ */ new Date()).toISOString(), originalTaskFile]
952
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
953
+ args: [now, parentId]
863
954
  });
864
955
  if (result.rowsAffected > 0) {
865
956
  process.stderr.write(
866
- `[review-cleanup] Cascaded original task to done: ${originalTaskFile}
957
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
867
958
  `
868
959
  );
869
960
  }
961
+ } else {
962
+ const fileName = taskFile.split("/").pop() ?? "";
963
+ const reviewPrefix = fileName.replace(".md", "");
964
+ const parts = reviewPrefix.split("-");
965
+ if (parts.length >= 3 && parts[0] === "review") {
966
+ const agent = parts[1];
967
+ const slug = parts.slice(2).join("-");
968
+ const originalTaskFile = `exe/${agent}/${slug}.md`;
969
+ const result = await client.execute({
970
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
971
+ args: [now, originalTaskFile]
972
+ });
973
+ if (result.rowsAffected > 0) {
974
+ process.stderr.write(
975
+ `[review-cleanup] Cascaded original task to done (legacy path): ${originalTaskFile}
976
+ `
977
+ );
978
+ }
979
+ }
870
980
  }
871
981
  } catch (err) {
872
982
  process.stderr.write(
@@ -1341,10 +1451,11 @@ async function updateTask(input) {
1341
1451
  try {
1342
1452
  const client = getClient();
1343
1453
  const taskTitle = String(row.title);
1454
+ const escaped = taskTitle.replace(/%/g, "\\%").replace(/_/g, "\\_");
1344
1455
  await client.execute({
1345
1456
  sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
1346
- WHERE title LIKE ? AND status IN ('open', 'in_progress')`,
1347
- args: [now, `%left%${taskTitle}%in_progress`]
1457
+ WHERE title LIKE ? ESCAPE '\\' AND status IN ('open', 'in_progress')`,
1458
+ args: [now, `%left '${escaped}' as in\\_progress%`]
1348
1459
  });
1349
1460
  } catch {
1350
1461
  }
@@ -1402,6 +1513,10 @@ async function updateTask(input) {
1402
1513
  taskFile,
1403
1514
  createdAt: String(row.created_at),
1404
1515
  updatedAt: now,
1516
+ budgetTokens: row.budget_tokens !== void 0 && row.budget_tokens !== null ? Number(row.budget_tokens) : null,
1517
+ budgetFallbackModel: row.budget_fallback_model !== void 0 && row.budget_fallback_model !== null ? String(row.budget_fallback_model) : null,
1518
+ tokensUsed: Number(row.tokens_used ?? 0),
1519
+ tokensWarnedAt: row.tokens_warned_at !== void 0 && row.tokens_warned_at !== null ? Number(row.tokens_warned_at) : null,
1405
1520
  nextTask
1406
1521
  };
1407
1522
  }
@@ -1438,7 +1553,7 @@ __export(active_agent_exports, {
1438
1553
  writeActiveAgent: () => writeActiveAgent
1439
1554
  });
1440
1555
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
1441
- import { execSync as execSync6 } from "child_process";
1556
+ import { execSync as execSync5 } from "child_process";
1442
1557
  import path13 from "path";
1443
1558
  function getMarkerPath() {
1444
1559
  return path13.join(CACHE_DIR, `active-agent-${getSessionKey()}.json`);
@@ -1488,7 +1603,7 @@ function getActiveAgent() {
1488
1603
  } catch {
1489
1604
  }
1490
1605
  try {
1491
- const sessionName = execSync6(
1606
+ const sessionName = execSync5(
1492
1607
  "tmux display-message -p '#{session_name}' 2>/dev/null",
1493
1608
  { encoding: "utf8", timeout: 2e3 }
1494
1609
  ).trim();