nexo-brain 2.2.0 → 2.3.0
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 +4 -4
- package/package.json +1 -1
- package/scripts/migrate-v1.7-to-v1.8.py +2 -2
- package/scripts/nexo-preflight.sh +236 -0
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/auto_update.py +25 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest.json +6 -13
- package/src/crons/sync.py +151 -6
- package/src/db/__init__.py +13 -0
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/_cron_runs.py +74 -0
- package/src/db/_episodic.py +40 -6
- package/src/db/_schema.py +64 -0
- package/src/db/_skills.py +514 -0
- package/src/hooks/session-stop.sh +13 -101
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/episodic_memory.py +5 -3
- package/src/plugins/schedule.py +212 -0
- package/src/plugins/skills.py +264 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/apply_findings.py +110 -8
- package/src/scripts/deep-sleep/collect.py +33 -11
- package/src/scripts/deep-sleep/extract-prompt.md +38 -0
- package/src/scripts/deep-sleep/extract.py +80 -8
- package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
- package/src/scripts/deep-sleep/synthesize.py +3 -1
- package/src/scripts/nexo-catchup.py +65 -29
- package/src/scripts/nexo-cron-wrapper.sh +53 -0
- package/src/scripts/nexo-daily-self-audit.py +4 -2
- package/src/scripts/nexo-deep-sleep.sh +66 -77
- package/src/scripts/nexo-evolution-run.py +13 -0
- package/src/scripts/nexo-learning-housekeep.py +156 -1
- package/src/scripts/nexo-learning-validator.py +19 -0
- package/src/scripts/nexo-postmortem-consolidator.py +3 -2
- package/src/scripts/nexo-sleep.py +16 -11
- package/src/scripts/nexo-synthesis.py +46 -3
- package/src/scripts/nexo-watchdog.sh +72 -19
- package/src/server.py +5 -1
- package/src/scripts/nexo-github-monitor.py +0 -256
package/README.md
CHANGED
|
@@ -283,13 +283,13 @@ NEXO Brain doesn't just respond — it runs 15 autonomous processes in the backg
|
|
|
283
283
|
| **prevent-sleep** | Always (daemon) | Keeps machine awake for nocturnal processes (caffeinate/systemd-inhibit) |
|
|
284
284
|
| **evolution** | Weekly (Sun) | Self-improvement proposals — NEXO suggests and applies enhancements |
|
|
285
285
|
| **followup-hygiene** | Weekly (Sun) | Normalizes statuses, flags stale followups, cleans orphans |
|
|
286
|
+
| **learning-housekeep** | 03:15 daily | Dedup learnings, adjust weights by usage, process overdue reviews, reconcile decision outcomes |
|
|
286
287
|
| **immune** | Every 30 min | Quarantine processing, memory promotion/rejection, synaptic pruning |
|
|
287
|
-
| **synthesis** |
|
|
288
|
-
| **
|
|
289
|
-
| **watchdog** | Every 5 min | Monitors services, LaunchAgents, and infrastructure health |
|
|
288
|
+
| **synthesis** | 06:00 daily | Memory synthesis — discovers cross-memory patterns |
|
|
289
|
+
| **watchdog** | Every 30 min | Monitors services, LaunchAgents, and infrastructure health |
|
|
290
290
|
| **auto-close-sessions** | Every 5 min | Cleans stale sessions |
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
Core processes are defined in `src/crons/manifest.json` and auto-synced to your system by `nexo_update`. On macOS they run via LaunchAgents; on Linux via systemd user timers. `tcc-approve`, `prevent-sleep`, and `backup` are platform/personal helpers — not in the manifest but listed above for completeness. Personal crons (your own scripts) are never touched by the sync. If your Mac was asleep during a scheduled process, the catch-up script re-runs everything in order when it wakes.
|
|
293
293
|
|
|
294
294
|
## Deep Sleep v2 — Overnight Learning (v2.1.0)
|
|
295
295
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — Cognitive co-operator for Claude Code. Memory, emotional intelligence, overnight learning (Deep Sleep), cron management, trust scoring, and adaptive calibration.",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,7 @@ MCP_OWNED_SECTIONS = [
|
|
|
39
39
|
"Dissonance",
|
|
40
40
|
"Disonancia",
|
|
41
41
|
"Observe the User",
|
|
42
|
-
"Observar a
|
|
42
|
+
"Observar a {{user}}", # legacy personal CLAUDE.md files
|
|
43
43
|
"Observar al Usuario",
|
|
44
44
|
"Change Log",
|
|
45
45
|
"Session Diary",
|
|
@@ -51,7 +51,7 @@ MCP_OWNED_SECTIONS = [
|
|
|
51
51
|
BOOTSTRAP_SECTIONS = [
|
|
52
52
|
"Startup",
|
|
53
53
|
"User Profile",
|
|
54
|
-
"
|
|
54
|
+
"{{user_name}}", # legacy personal CLAUDE.md files
|
|
55
55
|
"Formato",
|
|
56
56
|
"Format",
|
|
57
57
|
"Autonomy",
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# NEXO Preflight — CI / manual verification script
|
|
4
|
+
# Checks: Python syntax, shell syntax, manifest<->file consistency,
|
|
5
|
+
# manifest<->watchdog consistency
|
|
6
|
+
# Exit code: 0 if all PASS, 1 if any FAIL
|
|
7
|
+
# Usage: bash scripts/nexo-preflight.sh
|
|
8
|
+
# ============================================================================
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
12
|
+
SRC="$REPO_ROOT/src"
|
|
13
|
+
MANIFEST="$SRC/crons/manifest.json"
|
|
14
|
+
WATCHDOG="$SRC/scripts/nexo-watchdog.sh"
|
|
15
|
+
|
|
16
|
+
PASS=0
|
|
17
|
+
FAIL=0
|
|
18
|
+
WARN=0
|
|
19
|
+
|
|
20
|
+
pass() { echo " PASS $1"; ((PASS++)); }
|
|
21
|
+
fail() { echo " FAIL $1"; ((FAIL++)); }
|
|
22
|
+
warn() { echo " WARN $1"; ((WARN++)); }
|
|
23
|
+
|
|
24
|
+
echo "============================================================"
|
|
25
|
+
echo "NEXO Preflight — $(date '+%Y-%m-%d %H:%M:%S')"
|
|
26
|
+
echo "============================================================"
|
|
27
|
+
|
|
28
|
+
# ── 1. py_compile for all Python scripts in src/scripts/ ──────────────────
|
|
29
|
+
echo ""
|
|
30
|
+
# Non-core scripts to exclude from compilation checks
|
|
31
|
+
NON_CORE="check-context.py"
|
|
32
|
+
|
|
33
|
+
echo "--- Check 1: Python syntax (src/scripts/*.py) ---"
|
|
34
|
+
for pyfile in "$SRC"/scripts/*.py; do
|
|
35
|
+
# Skip " 2" duplicate files (backup copies)
|
|
36
|
+
[[ "$pyfile" == *" 2"* ]] && continue
|
|
37
|
+
[ -f "$pyfile" ] || continue
|
|
38
|
+
name=$(basename "$pyfile")
|
|
39
|
+
# Skip non-core scripts
|
|
40
|
+
for skip in $NON_CORE; do
|
|
41
|
+
[[ "$name" == "$skip" ]] && continue 2
|
|
42
|
+
done
|
|
43
|
+
if python3 -m py_compile "$pyfile" 2>/dev/null; then
|
|
44
|
+
pass "$name"
|
|
45
|
+
else
|
|
46
|
+
fail "$name — py_compile error"
|
|
47
|
+
fi
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
# ── 2. py_compile for auto_close_sessions.py ──────────────────────────────
|
|
51
|
+
echo ""
|
|
52
|
+
echo "--- Check 2: Python syntax (auto_close_sessions.py) ---"
|
|
53
|
+
ACS="$SRC/auto_close_sessions.py"
|
|
54
|
+
if [ -f "$ACS" ]; then
|
|
55
|
+
if python3 -m py_compile "$ACS" 2>/dev/null; then
|
|
56
|
+
pass "auto_close_sessions.py"
|
|
57
|
+
else
|
|
58
|
+
fail "auto_close_sessions.py — py_compile error"
|
|
59
|
+
fi
|
|
60
|
+
else
|
|
61
|
+
fail "auto_close_sessions.py — file not found"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# ── 3. bash -n for all shell scripts in src/scripts/ ─────────────────────
|
|
65
|
+
echo ""
|
|
66
|
+
echo "--- Check 3: Shell syntax (src/scripts/*.sh) ---"
|
|
67
|
+
for shfile in "$SRC"/scripts/*.sh; do
|
|
68
|
+
# Skip " 2" duplicate files (backup copies)
|
|
69
|
+
[[ "$shfile" == *" 2"* ]] && continue
|
|
70
|
+
[ -f "$shfile" ] || continue
|
|
71
|
+
name=$(basename "$shfile")
|
|
72
|
+
if bash -n "$shfile" 2>/dev/null; then
|
|
73
|
+
pass "$name"
|
|
74
|
+
else
|
|
75
|
+
fail "$name — bash -n syntax error"
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
# ── 4. Manifest<->file consistency ────────────────────────────────────────
|
|
80
|
+
echo ""
|
|
81
|
+
echo "--- Check 4: Manifest crons have existing script files ---"
|
|
82
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
83
|
+
fail "manifest.json not found at $MANIFEST"
|
|
84
|
+
else
|
|
85
|
+
# Extract script paths from manifest crons
|
|
86
|
+
cron_scripts=$(python3 -c "
|
|
87
|
+
import json, sys
|
|
88
|
+
try:
|
|
89
|
+
m = json.load(open('$MANIFEST'))
|
|
90
|
+
for c in m.get('crons', []):
|
|
91
|
+
print(c.get('id', '?') + '|' + c.get('script', ''))
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f'ERROR|{e}', file=sys.stderr)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
" 2>/dev/null)
|
|
96
|
+
|
|
97
|
+
if [ $? -ne 0 ]; then
|
|
98
|
+
fail "manifest.json — cannot parse JSON"
|
|
99
|
+
else
|
|
100
|
+
while IFS='|' read -r cron_id script_path; do
|
|
101
|
+
[ -z "$script_path" ] && continue
|
|
102
|
+
full_path="$SRC/$script_path"
|
|
103
|
+
if [ -f "$full_path" ]; then
|
|
104
|
+
pass "cron '$cron_id' -> $script_path exists"
|
|
105
|
+
else
|
|
106
|
+
fail "cron '$cron_id' -> $script_path NOT FOUND"
|
|
107
|
+
fi
|
|
108
|
+
done <<< "$cron_scripts"
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# ── 5. Manifest<->watchdog MONITORS consistency ──────────────────────────
|
|
113
|
+
echo ""
|
|
114
|
+
echo "--- Check 5: Manifest crons present in watchdog MONITORS ---"
|
|
115
|
+
if [ ! -f "$WATCHDOG" ]; then
|
|
116
|
+
fail "nexo-watchdog.sh not found at $WATCHDOG"
|
|
117
|
+
else
|
|
118
|
+
# The watchdog dynamically builds MONITORS from manifest.json via
|
|
119
|
+
# _build_monitors_from_manifest(). Verify that function exists and
|
|
120
|
+
# references the manifest, plus check that any hardcoded PERSONAL_MONITORS
|
|
121
|
+
# use valid com.nexo.* plist IDs.
|
|
122
|
+
|
|
123
|
+
if grep -q "_build_monitors_from_manifest" "$WATCHDOG"; then
|
|
124
|
+
pass "watchdog dynamically loads MONITORS from manifest.json"
|
|
125
|
+
else
|
|
126
|
+
fail "watchdog does NOT reference _build_monitors_from_manifest"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
if grep -q 'MANIFEST_FILE' "$WATCHDOG"; then
|
|
130
|
+
pass "watchdog references MANIFEST_FILE"
|
|
131
|
+
else
|
|
132
|
+
fail "watchdog does NOT reference MANIFEST_FILE for dynamic loading"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Check that any hardcoded personal monitors have valid format
|
|
136
|
+
personal_count=$(grep -c '|com\.nexo\.' "$WATCHDOG" 2>/dev/null || echo 0)
|
|
137
|
+
if [ "$personal_count" -gt 0 ]; then
|
|
138
|
+
pass "watchdog has $personal_count personal monitor entries"
|
|
139
|
+
else
|
|
140
|
+
pass "watchdog has no hardcoded personal monitors (all from manifest)"
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# ── 6. Manifest<->README consistency ─────────────────────────────────────
|
|
145
|
+
echo ""
|
|
146
|
+
echo "--- Check 6: Manifest crons mentioned in README ---"
|
|
147
|
+
README="$REPO_ROOT/README.md"
|
|
148
|
+
if [ -f "$README" ] && [ -f "$MANIFEST" ]; then
|
|
149
|
+
manifest_ids=$(python3 -c "
|
|
150
|
+
import json
|
|
151
|
+
m = json.load(open('$MANIFEST'))
|
|
152
|
+
for c in m.get('crons', []):
|
|
153
|
+
print(c['id'])
|
|
154
|
+
" 2>/dev/null)
|
|
155
|
+
|
|
156
|
+
for cid in $manifest_ids; do
|
|
157
|
+
if grep -qE "\*\*${cid}\*\*|${cid}" "$README" 2>/dev/null; then
|
|
158
|
+
pass "cron '$cid' documented in README"
|
|
159
|
+
else
|
|
160
|
+
fail "cron '$cid' NOT in README"
|
|
161
|
+
fi
|
|
162
|
+
done
|
|
163
|
+
else
|
|
164
|
+
warn "README.md or manifest.json not found, skipping"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# ── 7. Smoke tests ──────────────────────────────────────────────────────
|
|
168
|
+
echo ""
|
|
169
|
+
echo "--- Check 7: Smoke tests ---"
|
|
170
|
+
|
|
171
|
+
# 7a: catchup weekday conversion (manifest 0=Sunday -> python 6)
|
|
172
|
+
WEEKDAY_TEST=$(python3 -c "
|
|
173
|
+
# Simulate the conversion from catchup.py
|
|
174
|
+
manifest_weekday = 0 # Sunday in cron/launchd
|
|
175
|
+
py_weekday = (manifest_weekday - 1) % 7 # Should be 6 (Sunday in Python)
|
|
176
|
+
assert py_weekday == 6, f'Expected 6 (Sunday), got {py_weekday}'
|
|
177
|
+
# Also test Monday
|
|
178
|
+
assert (1 - 1) % 7 == 0, 'Monday should be 0'
|
|
179
|
+
# Saturday
|
|
180
|
+
assert (6 - 1) % 7 == 5, 'Saturday should be 5'
|
|
181
|
+
print('OK')
|
|
182
|
+
" 2>&1)
|
|
183
|
+
if [ "$WEEKDAY_TEST" = "OK" ]; then
|
|
184
|
+
pass "catchup weekday conversion (manifest 0=Sun -> python 6)"
|
|
185
|
+
else
|
|
186
|
+
fail "catchup weekday conversion: $WEEKDAY_TEST"
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
# 7b: change_log schema uses what_changed (not description)
|
|
190
|
+
SCHEMA_TEST=$(python3 -c "
|
|
191
|
+
import sys
|
|
192
|
+
sys.path.insert(0, '$SRC')
|
|
193
|
+
# Verify change_log columns match what learning-housekeep uses
|
|
194
|
+
with open('$SRC/db/_core.py') as f:
|
|
195
|
+
core = f.read()
|
|
196
|
+
if 'what_changed' in core:
|
|
197
|
+
print('OK')
|
|
198
|
+
else:
|
|
199
|
+
print('FAIL: what_changed not found in _core.py')
|
|
200
|
+
" 2>&1)
|
|
201
|
+
if [ "$SCHEMA_TEST" = "OK" ]; then
|
|
202
|
+
pass "change_log schema uses what_changed (matches reconciler)"
|
|
203
|
+
else
|
|
204
|
+
fail "change_log schema: $SCHEMA_TEST"
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
# 7c: reconciler queries use correct columns for change_log
|
|
208
|
+
RECONCILER_TEST=$(python3 -c "
|
|
209
|
+
with open('$SRC/scripts/nexo-learning-housekeep.py') as f:
|
|
210
|
+
code = f.read()
|
|
211
|
+
# Find the change_log section of _reconcile_decision_outcome
|
|
212
|
+
cl_section = code[code.index('# Check change_log'):code.index('return None', code.index('# Check change_log'))]
|
|
213
|
+
if 'what_changed LIKE' in cl_section:
|
|
214
|
+
print('OK')
|
|
215
|
+
else:
|
|
216
|
+
print('FAIL: change_log section does not use what_changed')
|
|
217
|
+
" 2>&1)
|
|
218
|
+
if [ "$RECONCILER_TEST" = "OK" ]; then
|
|
219
|
+
pass "reconciler uses correct change_log columns"
|
|
220
|
+
else
|
|
221
|
+
fail "reconciler columns: $RECONCILER_TEST"
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# ── Summary ───────────────────────────────────────────────────────────────
|
|
225
|
+
echo ""
|
|
226
|
+
echo "============================================================"
|
|
227
|
+
echo "Results: $PASS PASS, $FAIL FAIL, $WARN WARN"
|
|
228
|
+
echo "============================================================"
|
|
229
|
+
|
|
230
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
231
|
+
echo "PREFLIGHT FAILED"
|
|
232
|
+
exit 1
|
|
233
|
+
else
|
|
234
|
+
echo "PREFLIGHT OK"
|
|
235
|
+
exit 0
|
|
236
|
+
fi
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/auto_update.py
CHANGED
|
@@ -93,6 +93,27 @@ def _read_package_version() -> str:
|
|
|
93
93
|
return "unknown"
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
# ── Hook sync ────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
def _sync_hooks():
|
|
99
|
+
"""Copy hook scripts from src/hooks/ to NEXO_HOME/hooks/ after a git pull."""
|
|
100
|
+
import shutil
|
|
101
|
+
hooks_src = SRC_DIR / "hooks"
|
|
102
|
+
hooks_dest = NEXO_HOME / "hooks"
|
|
103
|
+
if not hooks_src.is_dir():
|
|
104
|
+
return
|
|
105
|
+
hooks_dest.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
synced = 0
|
|
107
|
+
for f in hooks_src.iterdir():
|
|
108
|
+
if f.is_file() and f.suffix == ".sh":
|
|
109
|
+
dest = hooks_dest / f.name
|
|
110
|
+
shutil.copy2(str(f), str(dest))
|
|
111
|
+
os.chmod(str(dest), 0o755)
|
|
112
|
+
synced += 1
|
|
113
|
+
if synced:
|
|
114
|
+
_log(f"Synced {synced} hook(s) to {hooks_dest}")
|
|
115
|
+
|
|
116
|
+
|
|
96
117
|
# ── Git-based auto-update ────────────────────────────────────────────
|
|
97
118
|
|
|
98
119
|
def _check_git_updates() -> str | None:
|
|
@@ -140,6 +161,10 @@ def _check_git_updates() -> str | None:
|
|
|
140
161
|
# Run DB migrations after pull
|
|
141
162
|
_run_db_migrations()
|
|
142
163
|
|
|
164
|
+
# Sync hooks to NEXO_HOME (nexo-brain.js copies them on install,
|
|
165
|
+
# but auto-update via git pull bypasses nexo-brain.js)
|
|
166
|
+
_sync_hooks()
|
|
167
|
+
|
|
143
168
|
msg = f"Auto-updated: {old_version} -> {new_version}" if old_version != new_version else f"Auto-updated (v{new_version}, new commits)"
|
|
144
169
|
_log(msg)
|
|
145
170
|
return msg
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/crons/manifest.json
CHANGED
|
@@ -70,35 +70,28 @@
|
|
|
70
70
|
{
|
|
71
71
|
"id": "followup-hygiene",
|
|
72
72
|
"script": "scripts/nexo-followup-hygiene.py",
|
|
73
|
-
"schedule": {"hour": 5, "minute": 0},
|
|
73
|
+
"schedule": {"hour": 5, "minute": 0, "weekday": 0},
|
|
74
74
|
"description": "Clean stale followups, archive completed, validate dates",
|
|
75
75
|
"core": true
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
"id": "synthesis",
|
|
79
79
|
"script": "scripts/nexo-synthesis.py",
|
|
80
|
-
"
|
|
81
|
-
"description": "
|
|
80
|
+
"schedule": {"hour": 6, "minute": 0},
|
|
81
|
+
"description": "Daily synthesis — cross-reference learnings, decisions, changes",
|
|
82
82
|
"core": true
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"id": "auto-close-sessions",
|
|
86
|
-
"script": "
|
|
86
|
+
"script": "auto_close_sessions.py",
|
|
87
87
|
"interval_seconds": 300,
|
|
88
88
|
"description": "Close stale sessions that lost their parent process",
|
|
89
89
|
"core": true
|
|
90
90
|
},
|
|
91
|
-
|
|
92
|
-
"id": "github-monitor",
|
|
93
|
-
"script": "scripts/nexo-github-monitor.py",
|
|
94
|
-
"schedule": {"hour": 8, "minute": 0},
|
|
95
|
-
"description": "Monitor GitHub repo — issues, PRs, stars, auto-respond",
|
|
96
|
-
"core": true
|
|
97
|
-
},
|
|
98
|
-
{
|
|
91
|
+
{
|
|
99
92
|
"id": "catchup",
|
|
100
93
|
"script": "scripts/nexo-catchup.py",
|
|
101
|
-
"
|
|
94
|
+
"run_at_load": true,
|
|
102
95
|
"description": "Morning catchup briefing for the user",
|
|
103
96
|
"core": true
|
|
104
97
|
}
|
package/src/crons/sync.py
CHANGED
|
@@ -42,15 +42,56 @@ def load_manifest() -> list[dict]:
|
|
|
42
42
|
return data.get("crons", [])
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _copy_script_to_nexo_home(src: Path) -> Path:
|
|
46
|
+
"""Copy a script from NEXO_CODE to NEXO_HOME/scripts/ for Sandbox compatibility.
|
|
47
|
+
|
|
48
|
+
macOS Sandbox blocks LaunchAgents from executing scripts in ~/Documents/.
|
|
49
|
+
We copy scripts to NEXO_HOME/scripts/ which is typically ~/claude/scripts/
|
|
50
|
+
or ~/.nexo/scripts/ — both outside the Sandbox restricted paths.
|
|
51
|
+
"""
|
|
52
|
+
dest_dir = NEXO_HOME / "scripts"
|
|
53
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
if src.is_dir():
|
|
56
|
+
import shutil
|
|
57
|
+
dest = dest_dir / src.name
|
|
58
|
+
if dest.exists():
|
|
59
|
+
shutil.rmtree(dest)
|
|
60
|
+
shutil.copytree(src, dest)
|
|
61
|
+
return dest
|
|
62
|
+
else:
|
|
63
|
+
dest = dest_dir / src.name
|
|
64
|
+
import shutil
|
|
65
|
+
shutil.copy2(src, dest)
|
|
66
|
+
dest.chmod(0o755)
|
|
67
|
+
return dest
|
|
68
|
+
|
|
69
|
+
|
|
45
70
|
def build_plist(cron: dict) -> dict:
|
|
46
71
|
"""Build a macOS LaunchAgent plist dict from a manifest entry."""
|
|
47
72
|
cron_id = cron["id"]
|
|
48
73
|
label = f"{LABEL_PREFIX}{cron_id}"
|
|
49
|
-
|
|
74
|
+
script_src = NEXO_CODE / cron["script"]
|
|
50
75
|
script_type = cron.get("type", "python")
|
|
51
76
|
|
|
77
|
+
# Copy scripts to NEXO_HOME/scripts/ to avoid macOS Sandbox restrictions
|
|
78
|
+
script_dest = _copy_script_to_nexo_home(script_src)
|
|
79
|
+
script_path = str(script_dest)
|
|
80
|
+
|
|
81
|
+
# Also copy the wrapper and any subdirectories (e.g., deep-sleep/)
|
|
82
|
+
wrapper_src = NEXO_CODE / "scripts" / "nexo-cron-wrapper.sh"
|
|
83
|
+
wrapper_dest = _copy_script_to_nexo_home(wrapper_src)
|
|
84
|
+
wrapper_path = str(wrapper_dest)
|
|
85
|
+
|
|
86
|
+
# Copy script subdirectories if they exist (e.g., deep-sleep/ for nexo-deep-sleep.sh)
|
|
87
|
+
script_name = script_src.stem # e.g., "nexo-deep-sleep"
|
|
88
|
+
subdir_name = script_name.replace("nexo-", "") # e.g., "deep-sleep"
|
|
89
|
+
subdir_src = NEXO_CODE / "scripts" / subdir_name
|
|
90
|
+
if subdir_src.is_dir():
|
|
91
|
+
_copy_script_to_nexo_home(subdir_src)
|
|
92
|
+
|
|
52
93
|
if script_type == "shell":
|
|
53
|
-
program_args = ["/bin/bash", script_path]
|
|
94
|
+
program_args = ["/bin/bash", wrapper_path, cron_id, "/bin/bash", script_path]
|
|
54
95
|
else:
|
|
55
96
|
# Find python3
|
|
56
97
|
python_candidates = [
|
|
@@ -64,7 +105,7 @@ def build_plist(cron: dict) -> dict:
|
|
|
64
105
|
if Path(p).exists():
|
|
65
106
|
python_bin = p
|
|
66
107
|
break
|
|
67
|
-
program_args = [python_bin, script_path]
|
|
108
|
+
program_args = ["/bin/bash", wrapper_path, cron_id, python_bin, script_path]
|
|
68
109
|
|
|
69
110
|
plist = {
|
|
70
111
|
"Label": label,
|
|
@@ -84,7 +125,9 @@ def build_plist(cron: dict) -> dict:
|
|
|
84
125
|
}
|
|
85
126
|
|
|
86
127
|
# Schedule
|
|
87
|
-
if "
|
|
128
|
+
if cron.get("run_at_load"):
|
|
129
|
+
plist["RunAtLoad"] = True
|
|
130
|
+
elif "interval_seconds" in cron:
|
|
88
131
|
plist["StartInterval"] = cron["interval_seconds"]
|
|
89
132
|
elif "schedule" in cron:
|
|
90
133
|
cal = {}
|
|
@@ -126,6 +169,8 @@ def plist_needs_update(existing_path: Path, new_plist: dict) -> bool:
|
|
|
126
169
|
return True
|
|
127
170
|
if existing.get("StartCalendarInterval") != new_plist.get("StartCalendarInterval"):
|
|
128
171
|
return True
|
|
172
|
+
if existing.get("RunAtLoad") != new_plist.get("RunAtLoad"):
|
|
173
|
+
return True
|
|
129
174
|
return False
|
|
130
175
|
|
|
131
176
|
|
|
@@ -157,8 +202,12 @@ def unload_plist(plist_path: Path, dry_run: bool):
|
|
|
157
202
|
|
|
158
203
|
|
|
159
204
|
def sync(dry_run: bool = False):
|
|
160
|
-
|
|
161
|
-
|
|
205
|
+
system = platform.system()
|
|
206
|
+
if system == "Linux":
|
|
207
|
+
sync_linux(dry_run)
|
|
208
|
+
return
|
|
209
|
+
if system != "Darwin":
|
|
210
|
+
log(f"Unsupported platform: {system}. Skipping.")
|
|
162
211
|
return
|
|
163
212
|
|
|
164
213
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -210,6 +259,102 @@ def sync(dry_run: bool = False):
|
|
|
210
259
|
log("Sync complete.")
|
|
211
260
|
|
|
212
261
|
|
|
262
|
+
def sync_linux(dry_run: bool = False):
|
|
263
|
+
"""Sync manifest to systemd user timers (Linux)."""
|
|
264
|
+
unit_dir = Path.home() / ".config" / "systemd" / "user"
|
|
265
|
+
unit_dir.mkdir(parents=True, exist_ok=True)
|
|
266
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
|
|
268
|
+
manifest_crons = load_manifest()
|
|
269
|
+
wrapper_src = NEXO_CODE / "scripts" / "nexo-cron-wrapper.sh"
|
|
270
|
+
wrapper_dest = _copy_script_to_nexo_home(wrapper_src)
|
|
271
|
+
|
|
272
|
+
log(f"Manifest: {len(manifest_crons)} core crons")
|
|
273
|
+
|
|
274
|
+
python_bin = "/usr/bin/python3"
|
|
275
|
+
for p in ["/usr/bin/python3", "/usr/local/bin/python3"]:
|
|
276
|
+
if Path(p).exists():
|
|
277
|
+
python_bin = p
|
|
278
|
+
break
|
|
279
|
+
|
|
280
|
+
for cron in manifest_crons:
|
|
281
|
+
cron_id = cron["id"]
|
|
282
|
+
script_src = NEXO_CODE / cron["script"]
|
|
283
|
+
script_dest = _copy_script_to_nexo_home(script_src)
|
|
284
|
+
script_type = cron.get("type", "python")
|
|
285
|
+
|
|
286
|
+
# Copy subdirectories
|
|
287
|
+
subdir_name = script_src.stem.replace("nexo-", "")
|
|
288
|
+
subdir_src = NEXO_CODE / "scripts" / subdir_name
|
|
289
|
+
if subdir_src.is_dir():
|
|
290
|
+
_copy_script_to_nexo_home(subdir_src)
|
|
291
|
+
|
|
292
|
+
if script_type == "shell":
|
|
293
|
+
exec_cmd = f"/bin/bash {wrapper_dest} {cron_id} /bin/bash {script_dest}"
|
|
294
|
+
else:
|
|
295
|
+
exec_cmd = f"/bin/bash {wrapper_dest} {cron_id} {python_bin} {script_dest}"
|
|
296
|
+
|
|
297
|
+
service_path = unit_dir / f"nexo-{cron_id}.service"
|
|
298
|
+
timer_path = unit_dir / f"nexo-{cron_id}.timer"
|
|
299
|
+
|
|
300
|
+
service_content = f"""[Unit]
|
|
301
|
+
Description=NEXO: {cron.get('description', cron_id)}
|
|
302
|
+
|
|
303
|
+
[Service]
|
|
304
|
+
Type=oneshot
|
|
305
|
+
ExecStart={exec_cmd}
|
|
306
|
+
Environment=NEXO_HOME={NEXO_HOME}
|
|
307
|
+
Environment=NEXO_CODE={NEXO_CODE}
|
|
308
|
+
Environment=HOME={Path.home()}
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
if cron.get("run_at_load"):
|
|
312
|
+
timer_spec = "OnBootSec=0"
|
|
313
|
+
elif "interval_seconds" in cron:
|
|
314
|
+
timer_spec = f"OnUnitActiveSec={cron['interval_seconds']}s\nOnBootSec=60s"
|
|
315
|
+
elif "schedule" in cron:
|
|
316
|
+
s = cron["schedule"]
|
|
317
|
+
h, m = s.get("hour", 0), s.get("minute", 0)
|
|
318
|
+
if "weekday" in s:
|
|
319
|
+
days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
320
|
+
timer_spec = f"OnCalendar={days[s['weekday']]} *-*-* {h:02d}:{m:02d}:00"
|
|
321
|
+
else:
|
|
322
|
+
timer_spec = f"OnCalendar=*-*-* {h:02d}:{m:02d}:00"
|
|
323
|
+
else:
|
|
324
|
+
log(f" SKIP {cron_id}: no schedule or interval")
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
timer_content = f"""[Unit]
|
|
328
|
+
Description=NEXO timer: {cron.get('description', cron_id)}
|
|
329
|
+
|
|
330
|
+
[Timer]
|
|
331
|
+
{timer_spec}
|
|
332
|
+
Persistent=true
|
|
333
|
+
|
|
334
|
+
[Install]
|
|
335
|
+
WantedBy=timers.target
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
if dry_run:
|
|
339
|
+
log(f" DRY-RUN: would install {cron_id}")
|
|
340
|
+
continue
|
|
341
|
+
|
|
342
|
+
service_path.write_text(service_content)
|
|
343
|
+
timer_path.write_text(timer_content)
|
|
344
|
+
log(f" Installed: {cron_id}")
|
|
345
|
+
|
|
346
|
+
if not dry_run:
|
|
347
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
|
|
348
|
+
for cron in manifest_crons:
|
|
349
|
+
subprocess.run(
|
|
350
|
+
["systemctl", "--user", "enable", "--now", f"nexo-{cron['id']}.timer"],
|
|
351
|
+
capture_output=True
|
|
352
|
+
)
|
|
353
|
+
log("systemd timers enabled.")
|
|
354
|
+
|
|
355
|
+
log("Sync complete.")
|
|
356
|
+
|
|
357
|
+
|
|
213
358
|
if __name__ == "__main__":
|
|
214
359
|
dry_run = "--dry-run" in sys.argv
|
|
215
360
|
if dry_run:
|
package/src/db/__init__.py
CHANGED
|
@@ -87,3 +87,16 @@ from db._evolution import (
|
|
|
87
87
|
insert_evolution_metric, get_latest_metrics,
|
|
88
88
|
insert_evolution_log, get_evolution_history, update_evolution_log_status,
|
|
89
89
|
)
|
|
90
|
+
|
|
91
|
+
# Cron execution history
|
|
92
|
+
from db._cron_runs import (
|
|
93
|
+
cron_run_start, cron_run_end, cron_runs_recent, cron_runs_summary,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Skills
|
|
97
|
+
from db._skills import (
|
|
98
|
+
create_skill, get_skill, list_skills, search_skills,
|
|
99
|
+
update_skill, delete_skill,
|
|
100
|
+
record_usage as record_skill_usage,
|
|
101
|
+
match_skills, merge_skills, get_skill_stats, decay_unused_skills,
|
|
102
|
+
)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|