@askexenow/exe-os 0.8.36 → 0.8.37

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 (62) hide show
  1. package/dist/bin/backfill-conversations.js +9 -1
  2. package/dist/bin/backfill-responses.js +9 -1
  3. package/dist/bin/cli.js +255 -31
  4. package/dist/bin/exe-assign.js +9 -1
  5. package/dist/bin/exe-boot.js +554 -23
  6. package/dist/bin/exe-dispatch.js +43 -3
  7. package/dist/bin/exe-export-behaviors.js +7 -0
  8. package/dist/bin/exe-gateway.js +57 -9
  9. package/dist/bin/exe-heartbeat.js +2 -1
  10. package/dist/bin/exe-kill.js +7 -0
  11. package/dist/bin/exe-launch-agent.js +8 -1
  12. package/dist/bin/exe-link.js +503 -12
  13. package/dist/bin/exe-pending-messages.js +2 -1
  14. package/dist/bin/exe-pending-reviews.js +2 -1
  15. package/dist/bin/exe-review.js +9 -1
  16. package/dist/bin/exe-search.js +9 -1
  17. package/dist/bin/exe-session-cleanup.js +11 -2
  18. package/dist/bin/git-sweep.js +9 -1
  19. package/dist/bin/graph-backfill.js +7 -0
  20. package/dist/bin/graph-export.js +7 -0
  21. package/dist/bin/install.js +35 -5
  22. package/dist/bin/scan-tasks.js +9 -1
  23. package/dist/bin/shard-migrate.js +7 -0
  24. package/dist/bin/wiki-sync.js +7 -0
  25. package/dist/gateway/index.js +57 -9
  26. package/dist/hooks/bug-report-worker.js +45 -5
  27. package/dist/hooks/commit-complete.js +9 -1
  28. package/dist/hooks/error-recall.js +10 -2
  29. package/dist/hooks/exe-heartbeat-hook.js +1 -1
  30. package/dist/hooks/ingest-worker.js +56 -8
  31. package/dist/hooks/ingest.js +1 -1
  32. package/dist/hooks/instructions-loaded.js +10 -2
  33. package/dist/hooks/notification.js +10 -2
  34. package/dist/hooks/post-compact.js +10 -2
  35. package/dist/hooks/pre-compact.js +10 -2
  36. package/dist/hooks/pre-tool-use.js +10 -2
  37. package/dist/hooks/prompt-ingest-worker.js +9 -1
  38. package/dist/hooks/prompt-submit.js +56 -8
  39. package/dist/hooks/response-ingest-worker.js +9 -1
  40. package/dist/hooks/session-end.js +10 -2
  41. package/dist/hooks/session-start.js +10 -2
  42. package/dist/hooks/stop.js +10 -2
  43. package/dist/hooks/subagent-stop.js +10 -2
  44. package/dist/hooks/summary-worker.js +512 -13
  45. package/dist/index.js +65 -15
  46. package/dist/lib/cloud-sync.js +502 -11
  47. package/dist/lib/exe-daemon.js +73 -23
  48. package/dist/lib/hybrid-search.js +9 -1
  49. package/dist/lib/messaging.js +43 -3
  50. package/dist/lib/store.js +9 -1
  51. package/dist/lib/tasks.js +47 -7
  52. package/dist/lib/tmux-routing.js +45 -3
  53. package/dist/mcp/server.js +73 -16
  54. package/dist/mcp/tools/create-task.js +48 -8
  55. package/dist/mcp/tools/deactivate-behavior.js +1 -1
  56. package/dist/mcp/tools/list-tasks.js +2 -1
  57. package/dist/mcp/tools/send-message.js +46 -6
  58. package/dist/mcp/tools/update-task.js +3 -2
  59. package/dist/runtime/index.js +54 -4
  60. package/dist/tui/App.js +54 -4
  61. package/package.json +2 -2
  62. package/src/commands/exe/afk.md +116 -0
package/dist/tui/App.js CHANGED
@@ -6028,6 +6028,7 @@ var init_capacity_monitor = __esm({
6028
6028
  // src/lib/tmux-routing.ts
6029
6029
  var tmux_routing_exports = {};
6030
6030
  __export(tmux_routing_exports, {
6031
+ acquireSpawnLock: () => acquireSpawnLock,
6031
6032
  employeeSessionName: () => employeeSessionName,
6032
6033
  ensureEmployee: () => ensureEmployee,
6033
6034
  extractRootExe: () => extractRootExe,
@@ -6042,6 +6043,7 @@ __export(tmux_routing_exports, {
6042
6043
  notifyParentExe: () => notifyParentExe,
6043
6044
  parseParentExe: () => parseParentExe,
6044
6045
  registerParentExe: () => registerParentExe,
6046
+ releaseSpawnLock: () => releaseSpawnLock,
6045
6047
  resolveExeSession: () => resolveExeSession,
6046
6048
  sendIntercom: () => sendIntercom,
6047
6049
  spawnEmployee: () => spawnEmployee,
@@ -6052,6 +6054,42 @@ import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirS
6052
6054
  import path19 from "path";
6053
6055
  import os6 from "os";
6054
6056
  import { fileURLToPath } from "url";
6057
+ import { unlinkSync as unlinkSync4 } from "fs";
6058
+ function spawnLockPath(sessionName) {
6059
+ return path19.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
6060
+ }
6061
+ function isProcessAlive(pid) {
6062
+ try {
6063
+ process.kill(pid, 0);
6064
+ return true;
6065
+ } catch {
6066
+ return false;
6067
+ }
6068
+ }
6069
+ function acquireSpawnLock(sessionName) {
6070
+ if (!existsSync11(SPAWN_LOCK_DIR)) {
6071
+ mkdirSync5(SPAWN_LOCK_DIR, { recursive: true });
6072
+ }
6073
+ const lockFile = spawnLockPath(sessionName);
6074
+ if (existsSync11(lockFile)) {
6075
+ try {
6076
+ const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
6077
+ const age = Date.now() - lock.timestamp;
6078
+ if (isProcessAlive(lock.pid) && age < 6e4) {
6079
+ return false;
6080
+ }
6081
+ } catch {
6082
+ }
6083
+ }
6084
+ writeFileSync5(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
6085
+ return true;
6086
+ }
6087
+ function releaseSpawnLock(sessionName) {
6088
+ try {
6089
+ unlinkSync4(spawnLockPath(sessionName));
6090
+ } catch {
6091
+ }
6092
+ }
6055
6093
  function resolveBehaviorsExporterScript() {
6056
6094
  try {
6057
6095
  const thisFile = fileURLToPath(import.meta.url);
@@ -6150,10 +6188,10 @@ function isEmployeeAlive(sessionName) {
6150
6188
  }
6151
6189
  function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
6152
6190
  const base = employeeSessionName(employeeName, exeSession);
6153
- if (!isAlive(base)) return 0;
6191
+ if (!isAlive(base) && acquireSpawnLock(base)) return 0;
6154
6192
  for (let i = 2; i <= maxInstances; i++) {
6155
6193
  const candidate = employeeSessionName(employeeName, exeSession, i);
6156
- if (!isAlive(candidate)) return i;
6194
+ if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
6157
6195
  }
6158
6196
  return null;
6159
6197
  }
@@ -6517,6 +6555,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6517
6555
  command: spawnCommand
6518
6556
  });
6519
6557
  if (spawnResult.error) {
6558
+ releaseSpawnLock(sessionName);
6520
6559
  return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
6521
6560
  }
6522
6561
  transport.pipeLog(sessionName, logFile);
@@ -6554,6 +6593,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6554
6593
  }
6555
6594
  }
6556
6595
  if (!booted) {
6596
+ releaseSpawnLock(sessionName);
6557
6597
  return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
6558
6598
  }
6559
6599
  if (!useExeAgent) {
@@ -6570,9 +6610,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
6570
6610
  pid: 0,
6571
6611
  registeredAt: (/* @__PURE__ */ new Date()).toISOString()
6572
6612
  });
6613
+ releaseSpawnLock(sessionName);
6573
6614
  return { sessionName };
6574
6615
  }
6575
- var SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
6616
+ var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
6576
6617
  var init_tmux_routing = __esm({
6577
6618
  "src/lib/tmux-routing.ts"() {
6578
6619
  "use strict";
@@ -6584,6 +6625,7 @@ var init_tmux_routing = __esm({
6584
6625
  init_provider_table();
6585
6626
  init_intercom_queue();
6586
6627
  init_plan_limits();
6628
+ SPAWN_LOCK_DIR = path19.join(os6.homedir(), ".exe-os", "spawn-locks");
6587
6629
  SESSION_CACHE = path19.join(os6.homedir(), ".exe-os", "session-cache");
6588
6630
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
6589
6631
  VERIFY_PANE_LINES = 200;
@@ -7734,7 +7776,8 @@ async function writeMemory(record) {
7734
7776
  has_error: record.has_error ? 1 : 0,
7735
7777
  raw_text: record.raw_text,
7736
7778
  vector: record.vector,
7737
- version: _nextVersion++,
7779
+ version: 0,
7780
+ // Placeholder — assigned atomically at flush time
7738
7781
  task_id: record.task_id ?? null,
7739
7782
  importance: record.importance ?? 5,
7740
7783
  status: record.status ?? "active",
@@ -7768,6 +7811,13 @@ async function flushBatch() {
7768
7811
  _flushing = true;
7769
7812
  try {
7770
7813
  const batch = _pendingRecords.slice(0);
7814
+ const client = getClient();
7815
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
7816
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
7817
+ for (const row of batch) {
7818
+ row.version = baseVersion++;
7819
+ }
7820
+ _nextVersion = baseVersion;
7771
7821
  const buildStmt = (row) => {
7772
7822
  const hasVector = row.vector !== null;
7773
7823
  const taskId = row.task_id ?? null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askexenow/exe-os",
3
- "version": "0.8.36",
3
+ "version": "0.8.37",
4
4
  "description": "AI employee operating system — persistent memory, task management, and multi-agent coordination for Claude Code.",
5
5
  "license": "CC-BY-NC-4.0",
6
6
  "type": "module",
@@ -59,7 +59,7 @@
59
59
  "./mcp/server": "./dist/mcp/server.js"
60
60
  },
61
61
  "engines": {
62
- "node": ">=18.0.0"
62
+ "node": ">=20.0.0"
63
63
  },
64
64
  "scripts": {
65
65
  "test": "vitest run",
@@ -0,0 +1,116 @@
1
+ ---
2
+ description: "Enter AFK mode — autonomously process reviews, dispatch work, and monitor employees on a timer"
3
+ allowed-tools: Bash, Read, Glob, Grep, mcp__exe-mem__list_tasks, mcp__exe-mem__get_task, mcp__exe-mem__close_task, mcp__exe-mem__create_task, mcp__exe-mem__update_task, mcp__exe-mem__store_memory, mcp__exe-mem__recall_my_memory, mcp__exe-mem__ask_team_memory, mcp__exe-mem__send_message, mcp__exe-mem__store_behavior, mcp__exe-mem__get_identity
4
+ argument-hint: "[stop | interval-minutes]"
5
+ ---
6
+
7
+ # AFK Mode — Autonomous COO Operations
8
+
9
+ The founder is stepping away. You (exe) now operate autonomously.
10
+
11
+ ## If argument is "stop"
12
+
13
+ Kill the background AFK loop and summarize:
14
+
15
+ ```bash
16
+ # Kill the AFK background loop
17
+ AFK_PID=$(cat ~/.exe-os/afk-loop.pid 2>/dev/null)
18
+ if [ -n "$AFK_PID" ]; then
19
+ kill "$AFK_PID" 2>/dev/null
20
+ rm -f ~/.exe-os/afk-loop.pid
21
+ echo "AFK loop stopped (PID $AFK_PID)"
22
+ else
23
+ echo "No active AFK loop found"
24
+ fi
25
+ ```
26
+
27
+ Then read the AFK log and summarize what happened:
28
+ ```bash
29
+ cat ~/.exe-os/afk-log.txt 2>/dev/null || echo "No AFK log found"
30
+ ```
31
+
32
+ Print: "AFK mode ended. Here's what happened while you were away:" then summarize all actions from the log. Clean up the log file.
33
+
34
+ ## Otherwise, start AFK mode
35
+
36
+ Default interval: 5 minutes. Override with argument (e.g., `/exe-afk 10` = every 10 min).
37
+
38
+ ### Step 1: Determine the exe tmux session name
39
+ ```bash
40
+ tmux display-message -p '#{session_name}' 2>/dev/null
41
+ ```
42
+
43
+ ### Step 2: Start the background ticker
44
+
45
+ This launches a background bash loop that sends review prompts into YOUR tmux session on the interval. Each send triggers a fresh Claude turn — you'll process reviews as if the founder typed the command.
46
+
47
+ ```bash
48
+ INTERVAL_SECONDS=$((${INTERVAL_MINUTES:-5} * 60))
49
+ EXE_SESSION="${SESSION_NAME}"
50
+
51
+ # Clean up any existing loop
52
+ OLD_PID=$(cat ~/.exe-os/afk-loop.pid 2>/dev/null)
53
+ [ -n "$OLD_PID" ] && kill "$OLD_PID" 2>/dev/null
54
+
55
+ # Start the AFK ticker as a background process
56
+ nohup bash -c "
57
+ echo \"AFK mode started at \$(date -Iseconds) — interval ${INTERVAL_SECONDS}s\" >> ~/.exe-os/afk-log.txt
58
+ while true; do
59
+ sleep ${INTERVAL_SECONDS}
60
+ # Send the review command into exe's tmux session
61
+ tmux send-keys -t '${EXE_SESSION}' '/exe-review' Enter 2>/dev/null
62
+ echo \"AFK tick at \$(date -Iseconds) — sent /exe-review to ${EXE_SESSION}\" >> ~/.exe-os/afk-log.txt
63
+ done
64
+ " > /dev/null 2>&1 &
65
+
66
+ # Save PID for stop command
67
+ echo $! > ~/.exe-os/afk-loop.pid
68
+ echo "AFK loop started — PID $!, interval ${INTERVAL_SECONDS}s, target session: ${EXE_SESSION}"
69
+ ```
70
+
71
+ ### Step 3: Confirm to founder
72
+
73
+ Print:
74
+ ```
75
+ 🤖 AFK MODE ACTIVE — checking every {N}m
76
+ Background loop PID: {PID}
77
+ Target session: {SESSION}
78
+
79
+ I'll automatically process reviews every {N} minutes.
80
+ Each tick sends /exe-review into this session — I'll handle it like a normal prompt.
81
+ Say /exe-afk stop when you return.
82
+ ```
83
+
84
+ ### Step 4: Do an immediate first tick
85
+
86
+ Don't wait for the first interval — process reviews RIGHT NOW before the founder leaves:
87
+
88
+ 1. Run: `node "$(npm root -g)/exe-os/dist/bin/exe-pending-reviews.js" 2>/dev/null`
89
+ 2. If reviews pending: process them (get_task → verify → close_task or reject)
90
+ 3. Check blocked tasks: `list_tasks status=blocked`
91
+ 4. Check idle employees: `tmux capture-pane` on active sessions
92
+
93
+ ## What /exe-review does when triggered by AFK tick
94
+
95
+ Each time the background loop sends `/exe-review` into the session, you (exe) will:
96
+
97
+ 1. **Check pending reviews** — process each one:
98
+ - Pull task with `get_task`
99
+ - Read deliverable (exe/output/, git log, worktree branches)
100
+ - Verify: tests pass? Output exists? Requirements met?
101
+ - PASS → `close_task` with result summary
102
+ - FAIL → `update_task` back to open with failure reason
103
+ - UNCERTAIN → skip, log for founder
104
+
105
+ 2. **Check stuck employees** — tmux capture-pane on active sessions
106
+ 3. **Check blocked tasks** — anything blocked >24h gets flagged
107
+ 4. **Dispatch next work** — idle employees with open tasks get assigned
108
+
109
+ ## Rules for autonomous operation
110
+ - NEVER make architectural decisions — those wait for founder
111
+ - NEVER modify code directly — dispatch to employees
112
+ - NEVER approve work you can't verify (no tests = no approval)
113
+ - DO process routine reviews (tests pass, output exists, requirements met)
114
+ - DO dispatch queued work to idle employees
115
+ - DO flag anything ambiguous for founder review
116
+ - If you hit context capacity (>80%), run `/exe-afk stop` and summarize