@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.
- package/dist/bin/backfill-conversations.js +9 -1
- package/dist/bin/backfill-responses.js +9 -1
- package/dist/bin/cli.js +255 -31
- package/dist/bin/exe-assign.js +9 -1
- package/dist/bin/exe-boot.js +554 -23
- package/dist/bin/exe-dispatch.js +43 -3
- package/dist/bin/exe-export-behaviors.js +7 -0
- package/dist/bin/exe-gateway.js +57 -9
- package/dist/bin/exe-heartbeat.js +2 -1
- package/dist/bin/exe-kill.js +7 -0
- package/dist/bin/exe-launch-agent.js +8 -1
- package/dist/bin/exe-link.js +503 -12
- package/dist/bin/exe-pending-messages.js +2 -1
- package/dist/bin/exe-pending-reviews.js +2 -1
- package/dist/bin/exe-review.js +9 -1
- package/dist/bin/exe-search.js +9 -1
- package/dist/bin/exe-session-cleanup.js +11 -2
- package/dist/bin/git-sweep.js +9 -1
- package/dist/bin/graph-backfill.js +7 -0
- package/dist/bin/graph-export.js +7 -0
- package/dist/bin/install.js +35 -5
- package/dist/bin/scan-tasks.js +9 -1
- package/dist/bin/shard-migrate.js +7 -0
- package/dist/bin/wiki-sync.js +7 -0
- package/dist/gateway/index.js +57 -9
- package/dist/hooks/bug-report-worker.js +45 -5
- package/dist/hooks/commit-complete.js +9 -1
- package/dist/hooks/error-recall.js +10 -2
- package/dist/hooks/exe-heartbeat-hook.js +1 -1
- package/dist/hooks/ingest-worker.js +56 -8
- package/dist/hooks/ingest.js +1 -1
- package/dist/hooks/instructions-loaded.js +10 -2
- package/dist/hooks/notification.js +10 -2
- package/dist/hooks/post-compact.js +10 -2
- package/dist/hooks/pre-compact.js +10 -2
- package/dist/hooks/pre-tool-use.js +10 -2
- package/dist/hooks/prompt-ingest-worker.js +9 -1
- package/dist/hooks/prompt-submit.js +56 -8
- package/dist/hooks/response-ingest-worker.js +9 -1
- package/dist/hooks/session-end.js +10 -2
- package/dist/hooks/session-start.js +10 -2
- package/dist/hooks/stop.js +10 -2
- package/dist/hooks/subagent-stop.js +10 -2
- package/dist/hooks/summary-worker.js +512 -13
- package/dist/index.js +65 -15
- package/dist/lib/cloud-sync.js +502 -11
- package/dist/lib/exe-daemon.js +73 -23
- package/dist/lib/hybrid-search.js +9 -1
- package/dist/lib/messaging.js +43 -3
- package/dist/lib/store.js +9 -1
- package/dist/lib/tasks.js +47 -7
- package/dist/lib/tmux-routing.js +45 -3
- package/dist/mcp/server.js +73 -16
- package/dist/mcp/tools/create-task.js +48 -8
- package/dist/mcp/tools/deactivate-behavior.js +1 -1
- package/dist/mcp/tools/list-tasks.js +2 -1
- package/dist/mcp/tools/send-message.js +46 -6
- package/dist/mcp/tools/update-task.js +3 -2
- package/dist/runtime/index.js +54 -4
- package/dist/tui/App.js +54 -4
- package/package.json +2 -2
- 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:
|
|
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.
|
|
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": ">=
|
|
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
|