nexo-brain 2.4.0 → 2.5.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.
- package/README.md +80 -4
- package/bin/nexo-brain.js +238 -12
- package/bin/nexo.js +55 -0
- package/community/skills/.gitkeep +1 -0
- package/package.json +11 -3
- package/src/auto_update.py +193 -9
- package/src/cli.py +719 -0
- package/src/cognitive/_ingest.py +1 -1
- package/src/cognitive/_memory.py +4 -4
- package/src/crons/manifest.json +8 -0
- package/src/dashboard/app.py +700 -35
- package/src/dashboard/templates/adaptive.html +112 -218
- package/src/dashboard/templates/artifacts.html +133 -0
- package/src/dashboard/templates/backups.html +136 -0
- package/src/dashboard/templates/base.html +413 -0
- package/src/dashboard/templates/calendar.html +523 -654
- package/src/dashboard/templates/chat.html +356 -0
- package/src/dashboard/templates/claims.html +259 -0
- package/src/dashboard/templates/cortex.html +262 -0
- package/src/dashboard/templates/credentials.html +128 -0
- package/src/dashboard/templates/crons.html +370 -0
- package/src/dashboard/templates/dashboard.html +383 -578
- package/src/dashboard/templates/dreams.html +252 -0
- package/src/dashboard/templates/email.html +160 -0
- package/src/dashboard/templates/evolution.html +189 -0
- package/src/dashboard/templates/feed.html +249 -0
- package/src/dashboard/templates/followup_health.html +170 -0
- package/src/dashboard/templates/graph.html +191 -269
- package/src/dashboard/templates/guard.html +259 -0
- package/src/dashboard/templates/inbox.html +220 -346
- package/src/dashboard/templates/memory.html +317 -197
- package/src/dashboard/templates/operations.html +521 -698
- package/src/dashboard/templates/plugins.html +185 -0
- package/src/dashboard/templates/rules.html +246 -0
- package/src/dashboard/templates/sentiment.html +247 -0
- package/src/dashboard/templates/sessions.html +215 -182
- package/src/dashboard/templates/skills.html +329 -0
- package/src/dashboard/templates/somatic.html +68 -172
- package/src/dashboard/templates/triggers.html +133 -0
- package/src/dashboard/templates/trust.html +360 -0
- package/src/db/__init__.py +5 -0
- package/src/db/_schema.py +16 -1
- package/src/db/_sessions.py +22 -0
- package/src/db/_skills.py +980 -274
- package/src/doctor/__init__.py +1 -0
- package/src/doctor/formatters.py +52 -0
- package/src/doctor/models.py +44 -0
- package/src/doctor/orchestrator.py +42 -0
- package/src/doctor/providers/__init__.py +1 -0
- package/src/doctor/providers/boot.py +206 -0
- package/src/doctor/providers/deep.py +292 -0
- package/src/doctor/providers/runtime.py +686 -0
- package/src/evolution_cycle.py +86 -6
- package/src/hooks/post-compact.sh +5 -1
- package/src/hooks/pre-compact.sh +1 -1
- package/src/plugins/doctor.py +36 -0
- package/src/plugins/evolution.py +11 -3
- package/src/plugins/skills.py +135 -175
- package/src/requirements.txt +1 -0
- package/src/script_registry.py +322 -0
- package/src/scripts/deep-sleep/apply_findings.py +63 -48
- package/src/scripts/deep-sleep/extract-prompt.md +14 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
- package/src/scripts/deep-sleep/synthesize.py +37 -1
- package/src/scripts/nexo-dashboard.sh +29 -0
- package/src/scripts/nexo-day-orchestrator.sh +139 -0
- package/src/scripts/nexo-evolution-run.py +141 -54
- package/src/scripts/nexo-learning-housekeep.py +1 -1
- package/src/scripts/nexo-watchdog.sh +1 -1
- package/src/server.py +9 -5
- package/src/skills/run-runtime-doctor/guide.md +12 -0
- package/src/skills/run-runtime-doctor/script.py +21 -0
- package/src/skills/run-runtime-doctor/skill.json +25 -0
- package/src/skills_runtime.py +347 -0
- package/src/tools_menu.py +3 -2
- package/src/tools_sessions.py +126 -0
- package/src/user_context.py +46 -0
- package/templates/nexo_helper.py +45 -0
- package/templates/script-template.py +44 -0
- package/templates/skill-script-template.py +39 -0
- package/templates/skill-template.md +33 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# NEXO Day Orchestrator — autonomous NEXO cycle every 15 min
|
|
4
|
+
# Schedule: keepAlive, self-enforced operating hours (default 8:00-23:00)
|
|
5
|
+
#
|
|
6
|
+
# This is NOT a Python script that simulates intelligence.
|
|
7
|
+
# This launches Claude Code as NEXO with full MCP access.
|
|
8
|
+
# NEXO thinks, acts, and reports — like any interactive session.
|
|
9
|
+
# ============================================================================
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
13
|
+
LOG_DIR="$NEXO_HOME/logs"
|
|
14
|
+
mkdir -p "$LOG_DIR" "$NEXO_HOME/operations"
|
|
15
|
+
|
|
16
|
+
# --- Configuration ---
|
|
17
|
+
CYCLE_INTERVAL=900 # 15 minutes between cycles
|
|
18
|
+
CYCLE_TIMEOUT=600 # 10 min max per cycle
|
|
19
|
+
MAX_TURNS=30 # Claude max turns per cycle
|
|
20
|
+
HOUR_START=8
|
|
21
|
+
HOUR_END=23
|
|
22
|
+
|
|
23
|
+
# --- Find Claude CLI ---
|
|
24
|
+
find_claude() {
|
|
25
|
+
for candidate in \
|
|
26
|
+
"$(command -v claude 2>/dev/null)" \
|
|
27
|
+
"$HOME/.claude/local/claude" \
|
|
28
|
+
"/opt/homebrew/bin/claude" \
|
|
29
|
+
"/usr/local/bin/claude"; do
|
|
30
|
+
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
|
31
|
+
echo "$candidate"
|
|
32
|
+
return 0
|
|
33
|
+
fi
|
|
34
|
+
done
|
|
35
|
+
return 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
CLAUDE=$(find_claude) || {
|
|
39
|
+
echo "$(date '+%Y-%m-%d %H:%M') ERROR: claude CLI not found" >&2
|
|
40
|
+
exit 1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# --- Prevent overlapping cycles ---
|
|
44
|
+
LOCKFILE="$NEXO_HOME/operations/.orchestrator.lock"
|
|
45
|
+
acquire_lock() {
|
|
46
|
+
if [ -f "$LOCKFILE" ]; then
|
|
47
|
+
local pid
|
|
48
|
+
pid=$(cat "$LOCKFILE" 2>/dev/null || echo "")
|
|
49
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
50
|
+
return 1 # Still running
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
echo $$ > "$LOCKFILE"
|
|
54
|
+
return 0
|
|
55
|
+
}
|
|
56
|
+
release_lock() { rm -f "$LOCKFILE"; }
|
|
57
|
+
|
|
58
|
+
# --- The orchestrator prompt ---
|
|
59
|
+
PROMPT='You are NEXO in autonomous orchestrator mode. The user is NOT present. You have 5 minutes max.
|
|
60
|
+
|
|
61
|
+
ABSOLUTE PRIORITY: act, do not list. If you can do something, do it. If you need the user, send email.
|
|
62
|
+
|
|
63
|
+
CHECKLIST (in this order):
|
|
64
|
+
|
|
65
|
+
1. OVERDUE FOLLOWUPS: nexo_reminders(filter="due") + nexo_reminders(filter="followups")
|
|
66
|
+
- NEXO tasks (verify, check, monitor) → DO THEM NOW
|
|
67
|
+
- Tasks needing user decision → accumulate for email
|
|
68
|
+
- Completed ones → nexo_followup_complete
|
|
69
|
+
|
|
70
|
+
2. EMAIL: nexo_email_inbox(unread_only=true, limit=10)
|
|
71
|
+
- Emails you can process → process them
|
|
72
|
+
- Important emails for user → accumulate for email
|
|
73
|
+
|
|
74
|
+
3. INFRASTRUCTURE: nexo_doctor(tier="runtime")
|
|
75
|
+
- If degraded/critical → try to fix
|
|
76
|
+
|
|
77
|
+
4. EMAIL TO USER (only if there is something to report):
|
|
78
|
+
- nexo_email_send with clean HTML summary
|
|
79
|
+
- Only what needs attention or decision
|
|
80
|
+
- Include what you ALREADY DID (not just pending items)
|
|
81
|
+
- If nothing relevant → DO NOT send email
|
|
82
|
+
- Max 1 email per cycle
|
|
83
|
+
|
|
84
|
+
5. DIARY: nexo_session_diary_write with what you did
|
|
85
|
+
|
|
86
|
+
RULES:
|
|
87
|
+
- DO NOT ask permission. autonomy=full
|
|
88
|
+
- DO NOT send empty or "all ok" emails
|
|
89
|
+
- DO NOT list things without acting
|
|
90
|
+
- If a followup is executable → execute it before reporting
|
|
91
|
+
- Use nexo_heartbeat at start
|
|
92
|
+
- Clean close: diary + nexo_stop'
|
|
93
|
+
|
|
94
|
+
# --- Main loop ---
|
|
95
|
+
echo "$(date '+%Y-%m-%d %H:%M') NEXO Day Orchestrator starting (PID $$)"
|
|
96
|
+
echo " Claude: $CLAUDE"
|
|
97
|
+
echo " Cycle: every ${CYCLE_INTERVAL}s, ${HOUR_START}:00-${HOUR_END}:00"
|
|
98
|
+
echo " Timeout: ${CYCLE_TIMEOUT}s, max turns: $MAX_TURNS"
|
|
99
|
+
|
|
100
|
+
while true; do
|
|
101
|
+
HOUR=$(date +%H | sed 's/^0//')
|
|
102
|
+
|
|
103
|
+
# Outside operating hours — sleep and check again
|
|
104
|
+
if [ "$HOUR" -lt "$HOUR_START" ] || [ "$HOUR" -ge "$HOUR_END" ]; then
|
|
105
|
+
sleep 300 # Check every 5 min if we're back in hours
|
|
106
|
+
continue
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Try to acquire lock
|
|
110
|
+
if ! acquire_lock; then
|
|
111
|
+
echo "$(date '+%Y-%m-%d %H:%M') Previous cycle still running. Skipping."
|
|
112
|
+
sleep "$CYCLE_INTERVAL"
|
|
113
|
+
continue
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
TIMESTAMP=$(date '+%Y-%m-%d_%H%M')
|
|
117
|
+
LOGFILE="$LOG_DIR/orchestrator-$TIMESTAMP.log"
|
|
118
|
+
echo "$(date '+%Y-%m-%d %H:%M') Cycle starting..."
|
|
119
|
+
|
|
120
|
+
# Launch Claude Code as NEXO
|
|
121
|
+
set +e
|
|
122
|
+
timeout "$CYCLE_TIMEOUT" "$CLAUDE" \
|
|
123
|
+
--dangerously-skip-permissions \
|
|
124
|
+
-p "$PROMPT" \
|
|
125
|
+
--max-turns "$MAX_TURNS" \
|
|
126
|
+
>>"$LOGFILE" 2>&1
|
|
127
|
+
EXIT_CODE=$?
|
|
128
|
+
set -e
|
|
129
|
+
|
|
130
|
+
echo "$(date '+%Y-%m-%d %H:%M') Cycle finished (exit $EXIT_CODE)" | tee -a "$LOGFILE"
|
|
131
|
+
|
|
132
|
+
release_lock
|
|
133
|
+
|
|
134
|
+
# Clean old logs (keep 7 days)
|
|
135
|
+
find "$LOG_DIR" -name "orchestrator-*.log" -mtime +7 -delete 2>/dev/null || true
|
|
136
|
+
|
|
137
|
+
# Sleep until next cycle
|
|
138
|
+
sleep "$CYCLE_INTERVAL"
|
|
139
|
+
done
|
|
@@ -34,22 +34,6 @@ SANDBOX_DIR = CLAUDE_DIR / "sandbox" / "workspace"
|
|
|
34
34
|
MAX_CONSECUTIVE_FAILURES = 3
|
|
35
35
|
MAX_SNAPSHOTS = 8
|
|
36
36
|
|
|
37
|
-
# ── Safe zones for AUTO execution ────────────────────────────────────────
|
|
38
|
-
# "review" mode (owner): broader zones, but nothing executes without approval
|
|
39
|
-
# "auto" mode (public users): restricted to user scripts and plugins ONLY
|
|
40
|
-
AUTO_SAFE_PREFIXES = [
|
|
41
|
-
str(CLAUDE_DIR / "scripts") + "/",
|
|
42
|
-
str(CLAUDE_DIR / "brain") + "/",
|
|
43
|
-
str(NEXO_CODE / "plugins") + "/",
|
|
44
|
-
str(CLAUDE_DIR / "logs") + "/",
|
|
45
|
-
str(CLAUDE_DIR / "coordination") + "/",
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
# Public mode: only user-created scripts — NEVER core, cortex, or plugins
|
|
49
|
-
AUTO_SAFE_PREFIXES_PUBLIC = [
|
|
50
|
-
str(CLAUDE_DIR / "scripts") + "/",
|
|
51
|
-
]
|
|
52
|
-
|
|
53
37
|
# ── Immutable files — NEVER touch (applies to ALL modes) ────────────────
|
|
54
38
|
IMMUTABLE_FILES = {
|
|
55
39
|
"db.py", "server.py", "plugin_loader.py", "nexo-watchdog.sh",
|
|
@@ -63,6 +47,52 @@ IMMUTABLE_FILES = {
|
|
|
63
47
|
"tools_task_history.py", "tools_menu.py",
|
|
64
48
|
}
|
|
65
49
|
|
|
50
|
+
|
|
51
|
+
def _repo_root() -> Path | None:
|
|
52
|
+
candidate = NEXO_CODE.parent
|
|
53
|
+
if (candidate / "package.json").exists():
|
|
54
|
+
return candidate
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _public_safe_prefixes() -> list[str]:
|
|
59
|
+
return [
|
|
60
|
+
str(CLAUDE_DIR / "scripts") + "/",
|
|
61
|
+
str(CLAUDE_DIR / "plugins") + "/",
|
|
62
|
+
str(CLAUDE_DIR / "skills") + "/",
|
|
63
|
+
str(CLAUDE_DIR / "skills-runtime") + "/",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _managed_safe_prefixes() -> list[str]:
|
|
68
|
+
prefixes = [
|
|
69
|
+
str(CLAUDE_DIR / "scripts") + "/",
|
|
70
|
+
str(CLAUDE_DIR / "plugins") + "/",
|
|
71
|
+
str(CLAUDE_DIR / "brain") + "/",
|
|
72
|
+
str(CLAUDE_DIR / "coordination") + "/",
|
|
73
|
+
str(CLAUDE_DIR / "logs") + "/",
|
|
74
|
+
str(CLAUDE_DIR / "skills") + "/",
|
|
75
|
+
str(CLAUDE_DIR / "skills-core") + "/",
|
|
76
|
+
str(CLAUDE_DIR / "skills-runtime") + "/",
|
|
77
|
+
str(NEXO_CODE) + "/",
|
|
78
|
+
]
|
|
79
|
+
repo_root = _repo_root()
|
|
80
|
+
if repo_root:
|
|
81
|
+
for rel in ("bin", "docs", "templates", "tests"):
|
|
82
|
+
prefixes.append(str(repo_root / rel) + "/")
|
|
83
|
+
return prefixes
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _normalize_mode(mode: str) -> str:
|
|
87
|
+
value = str(mode or "auto").strip().lower()
|
|
88
|
+
aliases = {
|
|
89
|
+
"owner": "managed",
|
|
90
|
+
"core": "managed",
|
|
91
|
+
"hybrid": "managed",
|
|
92
|
+
"manual": "review",
|
|
93
|
+
}
|
|
94
|
+
return aliases.get(value, value if value in {"auto", "review", "managed"} else "auto")
|
|
95
|
+
|
|
66
96
|
# ── Claude CLI path ──────────────────────────────────────────────────────
|
|
67
97
|
def _resolve_claude_cli() -> Path:
|
|
68
98
|
"""Find claude CLI: saved path > PATH > common locations."""
|
|
@@ -161,16 +191,18 @@ def call_claude_cli(prompt: str) -> str:
|
|
|
161
191
|
# ── File safety validation ───────────────────────────────────────────────
|
|
162
192
|
def is_safe_path(filepath: str, mode: str = "auto") -> bool:
|
|
163
193
|
"""Check if a file path is within safe zones and not immutable.
|
|
164
|
-
mode='auto' (public): restricted to
|
|
165
|
-
mode='
|
|
194
|
+
mode='auto' (public): restricted to personal automation surfaces.
|
|
195
|
+
mode='managed' (owner): broader repo/core surfaces with rollback.
|
|
196
|
+
mode='review': broader zones for proposal validation, but no execution.
|
|
166
197
|
"""
|
|
167
198
|
expanded = str(Path(filepath).expanduser().resolve())
|
|
168
199
|
filename = Path(expanded).name
|
|
200
|
+
mode = _normalize_mode(mode)
|
|
169
201
|
|
|
170
202
|
if filename in IMMUTABLE_FILES:
|
|
171
203
|
return False
|
|
172
204
|
|
|
173
|
-
prefixes =
|
|
205
|
+
prefixes = _managed_safe_prefixes() if mode in {"managed", "review"} else _public_safe_prefixes()
|
|
174
206
|
for prefix in prefixes:
|
|
175
207
|
resolved_prefix = str(Path(prefix).expanduser().resolve())
|
|
176
208
|
if expanded.startswith(resolved_prefix):
|
|
@@ -217,13 +249,13 @@ def validate_syntax(filepath: str) -> tuple[bool, str]:
|
|
|
217
249
|
|
|
218
250
|
|
|
219
251
|
# ── Apply a single change operation ──────────────────────────────────────
|
|
220
|
-
def apply_change(change: dict) -> tuple[bool, str]:
|
|
252
|
+
def apply_change(change: dict, mode: str = "auto") -> tuple[bool, str]:
|
|
221
253
|
"""Apply a single file change operation. Returns (success, message)."""
|
|
222
254
|
filepath = str(Path(change["file"]).expanduser())
|
|
223
255
|
operation = change.get("operation", "")
|
|
224
256
|
content = change.get("content", "")
|
|
225
257
|
|
|
226
|
-
if not is_safe_path(filepath):
|
|
258
|
+
if not is_safe_path(filepath, mode=mode):
|
|
227
259
|
return False, f"BLOCKED: {filepath} is outside safe zones or immutable"
|
|
228
260
|
|
|
229
261
|
try:
|
|
@@ -268,7 +300,7 @@ def apply_change(change: dict) -> tuple[bool, str]:
|
|
|
268
300
|
|
|
269
301
|
|
|
270
302
|
# ── Execute AUTO proposals ───────────────────────────────────────────────
|
|
271
|
-
def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connection) -> dict:
|
|
303
|
+
def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connection, mode: str = "auto") -> dict:
|
|
272
304
|
"""Execute an AUTO proposal with snapshot/apply/validate/rollback."""
|
|
273
305
|
changes = proposal.get("changes", [])
|
|
274
306
|
if not changes:
|
|
@@ -277,7 +309,7 @@ def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connecti
|
|
|
277
309
|
# Validate all paths first
|
|
278
310
|
for change in changes:
|
|
279
311
|
filepath = str(Path(change["file"]).expanduser())
|
|
280
|
-
if not is_safe_path(filepath):
|
|
312
|
+
if not is_safe_path(filepath, mode=mode):
|
|
281
313
|
return {"status": "blocked", "reason": f"Unsafe path: {filepath}"}
|
|
282
314
|
|
|
283
315
|
# Collect files to snapshot (existing files only)
|
|
@@ -298,7 +330,7 @@ def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connecti
|
|
|
298
330
|
all_results = []
|
|
299
331
|
try:
|
|
300
332
|
for change in changes:
|
|
301
|
-
success, msg = apply_change(change)
|
|
333
|
+
success, msg = apply_change(change, mode=mode)
|
|
302
334
|
all_results.append(msg)
|
|
303
335
|
log(f" {msg}")
|
|
304
336
|
if not success:
|
|
@@ -342,57 +374,102 @@ def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connecti
|
|
|
342
374
|
log(f" Removed created file: {filepath}")
|
|
343
375
|
|
|
344
376
|
return {
|
|
345
|
-
"status": "
|
|
377
|
+
"status": "rolled_back",
|
|
346
378
|
"snapshot_ref": snapshot_ref,
|
|
347
379
|
"files_changed": [],
|
|
348
380
|
"test_result": f"ROLLBACK: {e}; " + "; ".join(all_results),
|
|
349
381
|
}
|
|
350
382
|
|
|
351
383
|
|
|
352
|
-
# ──
|
|
353
|
-
def
|
|
354
|
-
|
|
355
|
-
|
|
384
|
+
# ── Followups for managed/review modes ──────────────────────────────────
|
|
385
|
+
def _insert_followup(conn: sqlite3.Connection, followup_id: str, description: str,
|
|
386
|
+
verification: str, due_date: str | None = None):
|
|
387
|
+
now_epoch = datetime.now().timestamp()
|
|
388
|
+
conn.execute(
|
|
389
|
+
"INSERT OR REPLACE INTO followups (id, description, date, status, verification, created_at, updated_at) "
|
|
390
|
+
"VALUES (?, ?, ?, 'PENDING', ?, ?, ?)",
|
|
391
|
+
(followup_id, description, due_date, verification, now_epoch, now_epoch)
|
|
392
|
+
)
|
|
393
|
+
conn.commit()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _create_cycle_followup(conn: sqlite3.Connection, cycle_num: int,
|
|
397
|
+
items: list[dict], analysis: str, mode: str):
|
|
398
|
+
"""Create a followup summarizing pending proposals or owner review items."""
|
|
356
399
|
tomorrow = (date.today() + timedelta(days=1)).isoformat()
|
|
357
400
|
followup_id = f"NF-EVO-C{cycle_num}"
|
|
358
401
|
|
|
359
402
|
public_items = [i for i in items if i.get("scope") == "public"]
|
|
360
403
|
local_items = [i for i in items if i.get("scope") != "public"]
|
|
361
404
|
|
|
362
|
-
|
|
405
|
+
title = "proposals to review" if mode == "review" else "items needing attention"
|
|
406
|
+
lines = [f"Evolution Cycle #{cycle_num} — {len(items)} {title}."]
|
|
363
407
|
lines.append(f"Analysis: {analysis[:200]}")
|
|
364
408
|
lines.append("")
|
|
365
409
|
|
|
366
410
|
if public_items:
|
|
367
411
|
lines.append(f"FOR EVERYONE ({len(public_items)}):")
|
|
368
412
|
for i, item in enumerate(public_items, 1):
|
|
369
|
-
|
|
413
|
+
status = item.get("status", "proposed").upper()
|
|
414
|
+
lines.append(f" {i}. [{status}] [{item['dimension']}] {item['action'][:120]}")
|
|
370
415
|
lines.append(f" Why: {item['reasoning'][:100]}")
|
|
416
|
+
if item.get("detail"):
|
|
417
|
+
lines.append(f" Detail: {item['detail'][:160]}")
|
|
371
418
|
lines.append("")
|
|
372
419
|
|
|
373
420
|
if local_items:
|
|
374
421
|
lines.append(f"FOR YOU ONLY ({len(local_items)}):")
|
|
375
422
|
for i, item in enumerate(local_items, 1):
|
|
376
|
-
|
|
423
|
+
status = item.get("status", "proposed").upper()
|
|
424
|
+
lines.append(f" {i}. [{status}] [{item['dimension']}] {item['action'][:120]}")
|
|
377
425
|
lines.append(f" Why: {item['reasoning'][:100]}")
|
|
426
|
+
if item.get("detail"):
|
|
427
|
+
lines.append(f" Detail: {item['detail'][:160]}")
|
|
378
428
|
|
|
379
429
|
description = "\n".join(lines)
|
|
380
430
|
|
|
381
431
|
try:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
now_epoch, now_epoch)
|
|
432
|
+
_insert_followup(
|
|
433
|
+
conn,
|
|
434
|
+
followup_id,
|
|
435
|
+
description,
|
|
436
|
+
f"SELECT * FROM evolution_log WHERE cycle_number={cycle_num}",
|
|
437
|
+
due_date=tomorrow,
|
|
389
438
|
)
|
|
390
|
-
conn.commit()
|
|
391
439
|
log(f" Followup {followup_id} created for {tomorrow}")
|
|
392
440
|
except Exception as e:
|
|
393
441
|
log(f" WARN: Failed to create followup: {e}")
|
|
394
442
|
|
|
395
443
|
|
|
444
|
+
def _create_failure_followup(conn: sqlite3.Connection, cycle_num: int, log_id: int,
|
|
445
|
+
proposal: dict, result: dict):
|
|
446
|
+
"""Create an incident-style followup for a failed or blocked AUTO proposal."""
|
|
447
|
+
followup_id = f"NF-EVO-L{log_id}"
|
|
448
|
+
lines = [
|
|
449
|
+
f"Evolution AUTO proposal failed in cycle #{cycle_num}.",
|
|
450
|
+
f"Action: {proposal.get('action', '')[:200]}",
|
|
451
|
+
f"Dimension: {proposal.get('dimension', 'other')}",
|
|
452
|
+
f"Status: {result.get('status', 'failed')}",
|
|
453
|
+
f"Reason: {(result.get('reason') or result.get('test_result') or 'unknown')[:400]}",
|
|
454
|
+
]
|
|
455
|
+
snapshot_ref = result.get("snapshot_ref")
|
|
456
|
+
if snapshot_ref:
|
|
457
|
+
lines.append(f"Snapshot: {snapshot_ref}")
|
|
458
|
+
description = "\n".join(lines)
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
_insert_followup(
|
|
462
|
+
conn,
|
|
463
|
+
followup_id,
|
|
464
|
+
description,
|
|
465
|
+
f"SELECT * FROM evolution_log WHERE id={log_id}",
|
|
466
|
+
due_date=(date.today() + timedelta(days=1)).isoformat(),
|
|
467
|
+
)
|
|
468
|
+
log(f" Failure followup {followup_id} created")
|
|
469
|
+
except Exception as e:
|
|
470
|
+
log(f" WARN: Failed to create failure followup: {e}")
|
|
471
|
+
|
|
472
|
+
|
|
396
473
|
# ── Main run ─────────────────────────────────────────────────────────────
|
|
397
474
|
def run():
|
|
398
475
|
log("=" * 60)
|
|
@@ -481,14 +558,12 @@ def run():
|
|
|
481
558
|
max_auto = max_auto_changes(objective.get("total_evolutions", 0))
|
|
482
559
|
auto_count = 0
|
|
483
560
|
auto_applied = 0
|
|
484
|
-
evolution_mode = objective.get("evolution_mode", "auto")
|
|
561
|
+
evolution_mode = _normalize_mode(objective.get("evolution_mode", "auto"))
|
|
485
562
|
|
|
486
563
|
conn = sqlite3.connect(str(NEXO_DB), timeout=10)
|
|
487
564
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
488
565
|
|
|
489
|
-
|
|
490
|
-
# In "auto" mode: execute AUTO proposals, log PROPOSE as proposed
|
|
491
|
-
review_items = []
|
|
566
|
+
followup_items = []
|
|
492
567
|
|
|
493
568
|
for p in proposals:
|
|
494
569
|
classification = p.get("classification", "propose")
|
|
@@ -498,30 +573,29 @@ def run():
|
|
|
498
573
|
scope = p.get("scope", "local") # "public" or "local"
|
|
499
574
|
|
|
500
575
|
if evolution_mode == "review":
|
|
501
|
-
# Owner mode: nothing executes, everything queued for review
|
|
502
576
|
log(f" QUEUED [{scope}]: {action[:80]}")
|
|
503
577
|
conn.execute(
|
|
504
578
|
"INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
|
|
505
579
|
"reasoning, status) VALUES (?, ?, ?, ?, ?, ?)",
|
|
506
580
|
(cycle_num, dimension, action, classification, reasoning, "pending_review")
|
|
507
581
|
)
|
|
508
|
-
|
|
582
|
+
followup_items.append({
|
|
509
583
|
"dimension": dimension,
|
|
510
584
|
"action": action,
|
|
511
585
|
"reasoning": reasoning,
|
|
512
586
|
"scope": scope,
|
|
513
587
|
"classification": classification,
|
|
588
|
+
"status": "pending_review",
|
|
514
589
|
})
|
|
515
590
|
|
|
516
591
|
elif classification == "auto" and auto_count < max_auto:
|
|
517
|
-
# Public mode: execute AUTO proposals
|
|
518
592
|
auto_count += 1
|
|
519
593
|
log(f" AUTO #{auto_count}/{max_auto}: {action[:80]}")
|
|
520
594
|
|
|
521
|
-
result = execute_auto_proposal(p, cycle_num, conn)
|
|
595
|
+
result = execute_auto_proposal(p, cycle_num, conn, mode=evolution_mode)
|
|
522
596
|
status = result["status"]
|
|
523
597
|
|
|
524
|
-
conn.execute(
|
|
598
|
+
cur = conn.execute(
|
|
525
599
|
"INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
|
|
526
600
|
"reasoning, status, files_changed, snapshot_ref, test_result) "
|
|
527
601
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
@@ -530,16 +604,20 @@ def run():
|
|
|
530
604
|
result.get("snapshot_ref", ""),
|
|
531
605
|
result.get("test_result", ""))
|
|
532
606
|
)
|
|
607
|
+
log_id = cur.lastrowid
|
|
533
608
|
|
|
534
609
|
if status == "applied":
|
|
535
610
|
auto_applied += 1
|
|
536
611
|
log(f" APPLIED successfully")
|
|
537
612
|
elif status == "blocked":
|
|
538
|
-
|
|
613
|
+
detail = result.get("reason") or result.get("test_result", "")
|
|
614
|
+
log(f" BLOCKED: {detail[:100]}")
|
|
615
|
+
_create_failure_followup(conn, cycle_num, log_id, p, result)
|
|
539
616
|
elif status == "skipped":
|
|
540
617
|
log(f" SKIPPED: {result.get('reason', '')}")
|
|
541
618
|
else:
|
|
542
|
-
log(f"
|
|
619
|
+
log(f" ROLLED BACK: {result.get('test_result', '')[:100]}")
|
|
620
|
+
_create_failure_followup(conn, cycle_num, log_id, p, result)
|
|
543
621
|
|
|
544
622
|
else:
|
|
545
623
|
# PROPOSE or over auto limit
|
|
@@ -554,12 +632,20 @@ def run():
|
|
|
554
632
|
"reasoning, status) VALUES (?, ?, ?, ?, ?, ?)",
|
|
555
633
|
(cycle_num, dimension, action, classification, reasoning, "proposed")
|
|
556
634
|
)
|
|
635
|
+
if evolution_mode in {"review", "managed"}:
|
|
636
|
+
followup_items.append({
|
|
637
|
+
"dimension": dimension,
|
|
638
|
+
"action": action,
|
|
639
|
+
"reasoning": reasoning,
|
|
640
|
+
"scope": scope,
|
|
641
|
+
"classification": classification,
|
|
642
|
+
"status": "proposed",
|
|
643
|
+
})
|
|
557
644
|
|
|
558
645
|
conn.commit()
|
|
559
646
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
_create_review_followup(conn, cycle_num, review_items, response.get("analysis", ""))
|
|
647
|
+
if evolution_mode in {"review", "managed"} and followup_items:
|
|
648
|
+
_create_cycle_followup(conn, cycle_num, followup_items, response.get("analysis", ""), evolution_mode)
|
|
563
649
|
|
|
564
650
|
# Update metrics
|
|
565
651
|
scores = response.get("dimension_scores", {})
|
|
@@ -590,6 +676,7 @@ def run():
|
|
|
590
676
|
objective.setdefault("history", []).insert(0, {
|
|
591
677
|
"cycle": cycle_num,
|
|
592
678
|
"date": str(date.today()),
|
|
679
|
+
"mode": evolution_mode,
|
|
593
680
|
"proposals": len(proposals),
|
|
594
681
|
"auto_count": auto_count,
|
|
595
682
|
"auto_applied": auto_applied,
|
|
@@ -70,7 +70,7 @@ def adjust_weights(conn):
|
|
|
70
70
|
priority = l["priority"] or "medium"
|
|
71
71
|
|
|
72
72
|
# Priority floor — critical learnings never drop below 0.5
|
|
73
|
-
priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}
|
|
73
|
+
priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}.get(priority, 0.1)
|
|
74
74
|
|
|
75
75
|
new_weight = old_weight
|
|
76
76
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# ============================================================================
|
|
3
3
|
# NEXO Watchdog — Comprehensive health monitor for all NEXO services
|
|
4
|
-
#
|
|
4
|
+
# Schedule: every 30 minutes (interval_seconds: 1800)
|
|
5
5
|
# ============================================================================
|
|
6
6
|
# Monitors ALL LaunchAgents, cron jobs, and background processes.
|
|
7
7
|
# Outputs: watchdog-status.json (machine), watchdog-report.txt (human),
|
package/src/server.py
CHANGED
|
@@ -8,6 +8,7 @@ import sys
|
|
|
8
8
|
from fastmcp import FastMCP
|
|
9
9
|
from db import init_db, rebuild_fts_index, get_db, close_db, fts_add_dir, fts_remove_dir, fts_list_dirs
|
|
10
10
|
from tools_sessions import handle_startup, handle_heartbeat, handle_status, handle_context_packet, handle_smart_startup_query
|
|
11
|
+
from user_context import get_context as _get_ctx
|
|
11
12
|
from tools_coordination import (
|
|
12
13
|
handle_track, handle_untrack, handle_files,
|
|
13
14
|
handle_send, handle_ask, handle_answer, handle_check_answer,
|
|
@@ -154,12 +155,17 @@ def _server_init():
|
|
|
154
155
|
mcp = FastMCP(
|
|
155
156
|
name="nexo",
|
|
156
157
|
instructions=(
|
|
157
|
-
"
|
|
158
|
+
f"{_get_ctx().assistant_name} — cognitive co-operator. Save important info from tool results before they clear.\n\n"
|
|
159
|
+
"## CRITICAL — do these or you WILL get corrected\n"
|
|
160
|
+
"- **Guard (MANDATORY before ANY code edit):** `nexo_guard_check(files='...', area='...')` BEFORE editing code. "
|
|
161
|
+
"No exceptions. Blocking rules→resolve first. `nexo_track(sid=SID, paths=[...])` before shared files\n"
|
|
162
|
+
"- **Skills (MANDATORY before multi-step tasks):** `nexo_skill_match(task)` to find reusable procedures. "
|
|
163
|
+
"If match found, read it and follow the steps. After completion, `nexo_skill_result(id, success, context)` to record outcome.\n"
|
|
164
|
+
"- **Learnings (MANDATORY on corrections):** When you discover a bug, pattern, or get corrected→`nexo_learning_add` IMMEDIATELY. "
|
|
165
|
+
"Do NOT batch. Do NOT wait until end of session.\n\n"
|
|
158
166
|
"## Rules\n"
|
|
159
167
|
"- **Heartbeat:** `nexo_heartbeat(sid=SID, task='...', context_hint='...')` every user msg. "
|
|
160
168
|
"React: DIARY REMINDER→write diary, VIBE:NEGATIVE→ultra-concise, AUTO-PRIME→read learnings\n"
|
|
161
|
-
"- **Guard:** `nexo_guard_check(files='...', area='...')` BEFORE editing code. "
|
|
162
|
-
"Blocking rules→resolve first. `nexo_track(sid=SID, paths=[...])` before shared files\n"
|
|
163
169
|
"- **Followups:** NEXO tasks, execute silently. 'done'/'all set'→`nexo_followup_complete` NOW. "
|
|
164
170
|
"Reminders=user's, alert when due\n"
|
|
165
171
|
"- **Observe:** correction→learning. 'tomorrow'→followup. person→entity. open topic→followup 3d\n"
|
|
@@ -175,8 +181,6 @@ mcp = FastMCP(
|
|
|
175
181
|
"write `nexo_session_diary_write(...)` with self_critique BEFORE responding. "
|
|
176
182
|
"Detect intent, not keywords. If session closes without diary, auto_close handles it.\n"
|
|
177
183
|
"- **Cortex:** `nexo_cortex_check` before budget/campaign/architecture changes\n"
|
|
178
|
-
"- **Skills:** before multi-step tasks, `nexo_skill_match(task)` to find reusable procedures. "
|
|
179
|
-
"If match found, read it and follow the steps. After completion, `nexo_skill_result(id, success, context)` to record outcome.\n"
|
|
180
184
|
"- **Dissonance:** user contradicts memory→`nexo_cognitive_dissonance`. Frustrated→force=True\n"
|
|
181
185
|
"- **Trust:** <40=paranoid verify twice, >80=fluid. Check: `nexo_cognitive_trust`"
|
|
182
186
|
),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Run Runtime Doctor
|
|
2
|
+
|
|
3
|
+
Use this skill when you want a fast health snapshot of the running NEXO system.
|
|
4
|
+
|
|
5
|
+
## Steps
|
|
6
|
+
1. Run the runtime doctor for the requested tier.
|
|
7
|
+
2. Review the degraded or critical checks first.
|
|
8
|
+
3. If the report recommends deterministic fixes, decide whether to run them explicitly.
|
|
9
|
+
|
|
10
|
+
## Gotchas
|
|
11
|
+
- A critical watchdog result reflects a real system issue, not just a stale skill.
|
|
12
|
+
- `all` is broader and slower than `runtime`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> int:
|
|
8
|
+
tier = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1] else "runtime"
|
|
9
|
+
nexo_code = os.environ.get("NEXO_CODE", "")
|
|
10
|
+
if not nexo_code:
|
|
11
|
+
print("NEXO_CODE not set", file=sys.stderr)
|
|
12
|
+
return 1
|
|
13
|
+
|
|
14
|
+
cli_py = os.path.join(nexo_code, "cli.py")
|
|
15
|
+
cmd = [sys.executable, cli_py, "doctor", "--tier", tier, "--json"]
|
|
16
|
+
result = subprocess.run(cmd, text=True)
|
|
17
|
+
return result.returncode
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "SK-RUN-RUNTIME-DOCTOR",
|
|
3
|
+
"name": "Run Runtime Doctor",
|
|
4
|
+
"description": "Runs the NEXO runtime doctor and returns the current health report.",
|
|
5
|
+
"level": "published",
|
|
6
|
+
"mode": "execute",
|
|
7
|
+
"source_kind": "core",
|
|
8
|
+
"execution_level": "read-only",
|
|
9
|
+
"approval_required": false,
|
|
10
|
+
"tags": ["doctor", "diagnostics", "runtime"],
|
|
11
|
+
"trigger_patterns": ["run doctor", "check runtime health", "diagnose nexo"],
|
|
12
|
+
"params_schema": {
|
|
13
|
+
"tier": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"required": false,
|
|
16
|
+
"default": "runtime",
|
|
17
|
+
"enum": ["boot", "runtime", "deep", "all"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"command_template": {
|
|
21
|
+
"argv": ["{{file_path}}", "{{tier}}"]
|
|
22
|
+
},
|
|
23
|
+
"executable_entry": "script.py",
|
|
24
|
+
"stable_after_uses": 10
|
|
25
|
+
}
|