nexo-brain 1.2.3 → 1.4.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 +10 -5
- package/package.json +1 -1
- package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
- package/src/cognitive.py +45 -0
- package/src/evolution_cycle.py +266 -0
- package/src/plugins/guard.py +235 -1
- package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -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-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.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/check-context.py +257 -0
- package/src/scripts/nexo-catchup.py +59 -5
- package/src/scripts/nexo-cognitive-decay.py +8 -0
- package/src/scripts/nexo-daily-self-audit.py +168 -183
- package/src/scripts/nexo-evolution-run.py +584 -0
- package/src/scripts/nexo-immune.py +108 -91
- package/src/scripts/nexo-learning-validator.py +226 -0
- package/src/scripts/nexo-postmortem-consolidator.py +230 -414
- package/src/scripts/nexo-sleep.py +283 -503
- package/src/scripts/nexo-synthesis.py +141 -432
- package/src/tools_sessions.py +20 -12
|
@@ -1,32 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
NEXO Daily Self-Audit
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
NEXO Daily Self-Audit v2
|
|
4
|
+
|
|
5
|
+
Stage A — Mechanical checks (Python pure, unchanged):
|
|
6
|
+
18 checks: overdue reminders, disk space, DB size, stale sessions, guard stats,
|
|
7
|
+
cognitive health, snapshot drift, etc. All pure queries, no intelligence needed.
|
|
8
|
+
|
|
9
|
+
Stage B — Interpretation (Claude CLI opus):
|
|
10
|
+
Takes the raw findings from Stage A and UNDERSTANDS them:
|
|
11
|
+
- Groups related findings
|
|
12
|
+
- Identifies root causes
|
|
13
|
+
- Prioritizes what actually matters
|
|
14
|
+
- Suggests specific actions
|
|
15
|
+
- Writes actionable summary
|
|
16
|
+
|
|
17
|
+
Runs via launchd at 7:00 AM daily.
|
|
6
18
|
"""
|
|
7
19
|
import json
|
|
20
|
+
import hashlib
|
|
8
21
|
import os
|
|
9
|
-
import re
|
|
10
22
|
import sqlite3
|
|
11
23
|
import subprocess
|
|
12
24
|
import sys
|
|
13
|
-
import hashlib
|
|
14
25
|
from datetime import datetime, timedelta
|
|
15
26
|
from pathlib import Path
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
LOG_DIR = NEXO_HOME_PATH / "logs"
|
|
28
|
+
LOG_DIR = Path.home() / ".nexo" / "logs"
|
|
19
29
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
20
30
|
LOG_FILE = LOG_DIR / "self-audit.log"
|
|
21
|
-
NEXO_DB =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
SNAPSHOT_GOLDEN = Path.home() / "claude" / "snapshots" / "golden" / "files" / "claude"
|
|
31
|
+
NEXO_DB = Path.home() / ".nexo" / "nexo.db"
|
|
32
|
+
project_DIR = Path.home() / "Documents" / "_PhpstormProjects" / "project"
|
|
33
|
+
HASH_REGISTRY = Path.home() / ".nexo" / "scripts" / ".watchdog-hashes"
|
|
34
|
+
SNAPSHOT_GOLDEN = Path.home() / ".nexo" / "snapshots" / "golden" / "files" / "claude"
|
|
26
35
|
RUNTIME_PREFLIGHT_SUMMARY = LOG_DIR / "runtime-preflight-summary.json"
|
|
27
36
|
WATCHDOG_SMOKE_SUMMARY = LOG_DIR / "watchdog-smoke-summary.json"
|
|
28
37
|
RESTORE_LOG = LOG_DIR / "snapshot-restores.log"
|
|
29
|
-
CORTEX_LOG_DIR = Path.home() / "
|
|
38
|
+
CORTEX_LOG_DIR = Path.home() / ".nexo" / "cortex" / "logs"
|
|
39
|
+
CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
|
|
30
40
|
|
|
31
41
|
findings = []
|
|
32
42
|
|
|
@@ -44,14 +54,17 @@ def finding(severity, area, msg):
|
|
|
44
54
|
log(f" [{severity}] {area}: {msg}")
|
|
45
55
|
|
|
46
56
|
|
|
47
|
-
#
|
|
57
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
58
|
+
# Stage A: Mechanical checks (UNCHANGED from v1 — all 18 checks)
|
|
59
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
60
|
+
|
|
48
61
|
def check_overdue_reminders():
|
|
49
62
|
if not NEXO_DB.exists():
|
|
50
63
|
return
|
|
51
64
|
conn = sqlite3.connect(str(NEXO_DB))
|
|
52
65
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
53
66
|
rows = conn.execute(
|
|
54
|
-
"SELECT description, date FROM reminders WHERE status='
|
|
67
|
+
"SELECT description, date FROM reminders WHERE status='PENDING' AND date < ? AND date != '' ORDER BY date",
|
|
55
68
|
(today,)
|
|
56
69
|
).fetchall()
|
|
57
70
|
conn.close()
|
|
@@ -59,14 +72,13 @@ def check_overdue_reminders():
|
|
|
59
72
|
finding("WARN", "reminders", f"{len(rows)} overdue: {', '.join(r[0][:40] for r in rows[:5])}")
|
|
60
73
|
|
|
61
74
|
|
|
62
|
-
# ── Check 2: Overdue followups ──────────────────────────────────────────
|
|
63
75
|
def check_overdue_followups():
|
|
64
76
|
if not NEXO_DB.exists():
|
|
65
77
|
return
|
|
66
78
|
conn = sqlite3.connect(str(NEXO_DB))
|
|
67
79
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
68
80
|
rows = conn.execute(
|
|
69
|
-
"SELECT description, date FROM followups WHERE status='
|
|
81
|
+
"SELECT description, date FROM followups WHERE status='PENDING' AND date < ? AND date != '' ORDER BY date",
|
|
70
82
|
(today,)
|
|
71
83
|
).fetchall()
|
|
72
84
|
conn.close()
|
|
@@ -74,20 +86,18 @@ def check_overdue_followups():
|
|
|
74
86
|
finding("WARN", "followups", f"{len(rows)} overdue: {', '.join(r[0][:40] for r in rows[:5])}")
|
|
75
87
|
|
|
76
88
|
|
|
77
|
-
# ── Check 3: Git uncommitted changes in project repo ───────────────────
|
|
78
89
|
def check_uncommitted_changes():
|
|
79
|
-
if not
|
|
90
|
+
if not project_DIR.exists():
|
|
80
91
|
return
|
|
81
92
|
result = subprocess.run(
|
|
82
93
|
["git", "status", "--porcelain"],
|
|
83
|
-
cwd=str(
|
|
94
|
+
cwd=str(project_DIR), capture_output=True, text=True
|
|
84
95
|
)
|
|
85
96
|
lines = [l for l in result.stdout.strip().split("\n") if l.strip()]
|
|
86
97
|
if len(lines) > 10:
|
|
87
|
-
finding("WARN", "git", f"{len(lines)} uncommitted changes in
|
|
98
|
+
finding("WARN", "git", f"{len(lines)} uncommitted changes in project repo")
|
|
88
99
|
|
|
89
100
|
|
|
90
|
-
# ── Check 4: Cron error logs (last 24h) ────────────────────────────────
|
|
91
101
|
def check_cron_errors():
|
|
92
102
|
if not NEXO_DB.exists():
|
|
93
103
|
return
|
|
@@ -102,9 +112,8 @@ def check_cron_errors():
|
|
|
102
112
|
finding("ERROR", "crons", f"{len(rows)} cron errors in last 24h")
|
|
103
113
|
|
|
104
114
|
|
|
105
|
-
# ── Check 5: Evolution failures ─────────────────────────────────────────
|
|
106
115
|
def check_evolution_health():
|
|
107
|
-
obj_file = Path.home() / "
|
|
116
|
+
obj_file = Path.home() / ".nexo" / "cortex" / "evolution-objective.json"
|
|
108
117
|
if not obj_file.exists():
|
|
109
118
|
return
|
|
110
119
|
obj = json.loads(obj_file.read_text())
|
|
@@ -115,7 +124,6 @@ def check_evolution_health():
|
|
|
115
124
|
finding("ERROR", "evolution", f"Evolution DISABLED: {obj.get('disabled_reason', 'unknown')}")
|
|
116
125
|
|
|
117
126
|
|
|
118
|
-
# ── Check 6: Disk space ────────────────────────────────────────────────
|
|
119
127
|
def check_disk_space():
|
|
120
128
|
result = subprocess.run(["df", "-h", "/"], capture_output=True, text=True)
|
|
121
129
|
for line in result.stdout.strip().split("\n")[1:]:
|
|
@@ -128,7 +136,6 @@ def check_disk_space():
|
|
|
128
136
|
finding("WARN", "disk", f"Root disk at {usage_pct}% capacity")
|
|
129
137
|
|
|
130
138
|
|
|
131
|
-
# ── Check 7: NEXO DB size ──────────────────────────────────────────────
|
|
132
139
|
def check_db_size():
|
|
133
140
|
if NEXO_DB.exists():
|
|
134
141
|
size_mb = NEXO_DB.stat().st_size / (1024 * 1024)
|
|
@@ -136,7 +143,6 @@ def check_db_size():
|
|
|
136
143
|
finding("WARN", "database", f"nexo.db is {size_mb:.1f} MB — consider cleanup")
|
|
137
144
|
|
|
138
145
|
|
|
139
|
-
# ── Check 8: Stale sessions ────────────────────────────────────────────
|
|
140
146
|
def check_stale_sessions():
|
|
141
147
|
if not NEXO_DB.exists():
|
|
142
148
|
return
|
|
@@ -152,15 +158,12 @@ def check_stale_sessions():
|
|
|
152
158
|
finding("INFO", "sessions", f"{len(rows)} stale sessions (no heartbeat >2h)")
|
|
153
159
|
|
|
154
160
|
|
|
155
|
-
# ── Check 9: Error repetition rate (Guard) ─────────────────────────────
|
|
156
161
|
def check_repetition_rate():
|
|
157
|
-
"""Alert if >30% of learnings in last 3 days are repetitions."""
|
|
158
162
|
if not NEXO_DB.exists():
|
|
159
163
|
return
|
|
160
164
|
conn = sqlite3.connect(str(NEXO_DB))
|
|
161
|
-
cutoff_3d = (datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S")
|
|
162
165
|
cutoff_epoch = (datetime.now() - timedelta(days=3)).timestamp()
|
|
163
|
-
|
|
166
|
+
cutoff_3d = (datetime.now() - timedelta(days=3)).strftime("%Y-%m-%d %H:%M:%S")
|
|
164
167
|
new_learnings = conn.execute(
|
|
165
168
|
"SELECT COUNT(*) FROM learnings WHERE created_at > ?", (cutoff_epoch,)
|
|
166
169
|
).fetchone()[0]
|
|
@@ -168,44 +171,34 @@ def check_repetition_rate():
|
|
|
168
171
|
"SELECT COUNT(*) FROM error_repetitions WHERE created_at > ?", (cutoff_3d,)
|
|
169
172
|
).fetchone()[0]
|
|
170
173
|
conn.close()
|
|
171
|
-
|
|
172
174
|
if new_learnings > 0:
|
|
173
175
|
rate = repetitions / new_learnings
|
|
174
176
|
if rate > 0.30:
|
|
175
|
-
finding("ERROR", "guard", f"Repetition rate {rate:.0%}
|
|
177
|
+
finding("ERROR", "guard", f"Repetition rate {rate:.0%} ({repetitions}/{new_learnings})")
|
|
176
178
|
elif rate > 0.20:
|
|
177
|
-
finding("WARN", "guard", f"Repetition rate {rate:.0%}
|
|
179
|
+
finding("WARN", "guard", f"Repetition rate {rate:.0%} ({repetitions}/{new_learnings})")
|
|
178
180
|
|
|
179
181
|
|
|
180
|
-
# ── Check 10: Unused learnings ─────────────────────────────────────────
|
|
181
182
|
def check_unused_learnings():
|
|
182
|
-
"""Find learnings >7 days old never returned by guard_check."""
|
|
183
183
|
if not NEXO_DB.exists():
|
|
184
184
|
return
|
|
185
185
|
conn = sqlite3.connect(str(NEXO_DB))
|
|
186
186
|
cutoff_epoch = (datetime.now() - timedelta(days=7)).timestamp()
|
|
187
|
-
|
|
188
187
|
old_learnings = conn.execute(
|
|
189
188
|
"SELECT COUNT(*) FROM learnings WHERE created_at < ?", (cutoff_epoch,)
|
|
190
189
|
).fetchone()[0]
|
|
191
190
|
total_checks = conn.execute("SELECT COUNT(*) FROM guard_checks").fetchone()[0]
|
|
192
191
|
conn.close()
|
|
193
|
-
|
|
194
192
|
if total_checks == 0 and old_learnings > 10:
|
|
195
|
-
finding("WARN", "guard", f"Guard never used — {old_learnings} learnings
|
|
196
|
-
elif total_checks > 0 and total_checks < 5:
|
|
197
|
-
finding("INFO", "guard", f"Only {total_checks} guard checks performed — aim for >5 per session")
|
|
193
|
+
finding("WARN", "guard", f"Guard never used — {old_learnings} learnings idle")
|
|
198
194
|
|
|
199
195
|
|
|
200
|
-
# ── Check 11: Memory reviews due ────────────────────────────────────────
|
|
201
196
|
def check_memory_reviews():
|
|
202
|
-
"""Alert when decisions/learnings are due for review."""
|
|
203
197
|
if not NEXO_DB.exists():
|
|
204
198
|
return
|
|
205
199
|
conn = sqlite3.connect(str(NEXO_DB))
|
|
206
200
|
now_epoch = datetime.now().timestamp()
|
|
207
201
|
now_iso = datetime.now().isoformat(timespec="seconds")
|
|
208
|
-
|
|
209
202
|
try:
|
|
210
203
|
due_learnings = conn.execute(
|
|
211
204
|
"SELECT COUNT(*) FROM learnings WHERE review_due_at IS NOT NULL AND status != 'superseded' AND review_due_at <= ?",
|
|
@@ -219,49 +212,39 @@ def check_memory_reviews():
|
|
|
219
212
|
conn.close()
|
|
220
213
|
return
|
|
221
214
|
conn.close()
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
finding("INFO", "memory", f"{total_due} memory reviews due ({due_decisions} decisions, {due_learnings} learnings)")
|
|
215
|
+
total = due_learnings + due_decisions
|
|
216
|
+
if total >= 10:
|
|
217
|
+
finding("WARN", "memory", f"{total} reviews due ({due_decisions} decisions, {due_learnings} learnings)")
|
|
218
|
+
elif total > 0:
|
|
219
|
+
finding("INFO", "memory", f"{total} reviews due")
|
|
228
220
|
|
|
229
221
|
|
|
230
|
-
def _sha256(path
|
|
222
|
+
def _sha256(path):
|
|
231
223
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
232
224
|
|
|
233
225
|
|
|
234
|
-
# ── Check 12: Watchdog registry sanity ──────────────────────────────────
|
|
235
226
|
def check_watchdog_registry():
|
|
236
227
|
if not HASH_REGISTRY.exists():
|
|
237
|
-
finding("WARN", "watchdog", "hash registry missing")
|
|
238
228
|
return
|
|
239
229
|
text = HASH_REGISTRY.read_text(errors="ignore")
|
|
240
230
|
forbidden = ["CLAUDE.md", "db.py", "server.py", "plugin_loader.py", "cortex-wrapper.py"]
|
|
241
231
|
bad = [name for name in forbidden if name in text]
|
|
242
232
|
if bad:
|
|
243
|
-
finding("ERROR", "watchdog", f"mutable files still protected
|
|
233
|
+
finding("ERROR", "watchdog", f"mutable files still protected: {', '.join(bad)}")
|
|
244
234
|
|
|
245
235
|
|
|
246
|
-
# ── Check 13: Snapshot drift on protected recovery files ────────────────
|
|
247
236
|
def check_snapshot_sync():
|
|
248
237
|
pairs = [
|
|
249
|
-
(
|
|
250
|
-
(Path.home() / "
|
|
251
|
-
(Path.home() / "
|
|
238
|
+
(Path.home() / ".nexo" / "db.py", SNAPSHOT_GOLDEN / "db.py"),
|
|
239
|
+
(Path.home() / ".nexo" / "cortex" / "cortex-wrapper.py", SNAPSHOT_GOLDEN / "cortex" / "cortex-wrapper.py"),
|
|
240
|
+
(Path.home() / ".nexo" / "cortex" / "evolution_cycle.py", SNAPSHOT_GOLDEN / "cortex" / "evolution_cycle.py"),
|
|
252
241
|
]
|
|
253
|
-
drift = [
|
|
254
|
-
|
|
255
|
-
if not live.exists() or not snap.exists():
|
|
256
|
-
drift.append(live.name)
|
|
257
|
-
continue
|
|
258
|
-
if _sha256(live) != _sha256(snap):
|
|
259
|
-
drift.append(live.name)
|
|
242
|
+
drift = [live.name for live, snap in pairs
|
|
243
|
+
if not live.exists() or not snap.exists() or _sha256(live) != _sha256(snap)]
|
|
260
244
|
if drift:
|
|
261
245
|
finding("WARN", "snapshots", f"golden snapshot drift: {', '.join(drift)}")
|
|
262
246
|
|
|
263
247
|
|
|
264
|
-
# ── Check 14: Recent restore activity ───────────────────────────────────
|
|
265
248
|
def check_restore_activity():
|
|
266
249
|
if not RESTORE_LOG.exists():
|
|
267
250
|
return
|
|
@@ -270,9 +253,7 @@ def check_restore_activity():
|
|
|
270
253
|
recent_day = 0
|
|
271
254
|
recent_hour = 0
|
|
272
255
|
for line in RESTORE_LOG.read_text(errors="ignore").splitlines():
|
|
273
|
-
if not line.startswith("["):
|
|
274
|
-
continue
|
|
275
|
-
if "/.codex/memories/nexo-" in line:
|
|
256
|
+
if not line.startswith("[") or "/.codex/memories/nexo-" in line:
|
|
276
257
|
continue
|
|
277
258
|
try:
|
|
278
259
|
ts = datetime.strptime(line[1:20], "%Y-%m-%d %H:%M:%S")
|
|
@@ -283,37 +264,29 @@ def check_restore_activity():
|
|
|
283
264
|
if line[1:14] == current_hour_prefix:
|
|
284
265
|
recent_hour += 1
|
|
285
266
|
if recent_hour > 2:
|
|
286
|
-
finding("ERROR", "restore", f"{recent_hour}
|
|
267
|
+
finding("ERROR", "restore", f"{recent_hour} restores in last hour")
|
|
287
268
|
elif recent_day > 5:
|
|
288
|
-
finding("WARN", "restore", f"{recent_day}
|
|
289
|
-
elif recent_day > 0:
|
|
290
|
-
finding("INFO", "restore", f"{recent_day} snapshot restores in last 24h (historical activity)")
|
|
269
|
+
finding("WARN", "restore", f"{recent_day} restores in last 24h")
|
|
291
270
|
|
|
292
271
|
|
|
293
|
-
# ── Check 15: Bad model responses ───────────────────────────────────────
|
|
294
272
|
def check_bad_responses():
|
|
295
273
|
if not CORTEX_LOG_DIR.exists():
|
|
296
274
|
return
|
|
297
275
|
cutoff = datetime.now() - timedelta(days=1)
|
|
298
|
-
bad = [
|
|
299
|
-
|
|
300
|
-
if datetime.fromtimestamp(p.stat().st_mtime) >= cutoff
|
|
301
|
-
]
|
|
276
|
+
bad = [p for p in CORTEX_LOG_DIR.glob("bad-response-*.json")
|
|
277
|
+
if datetime.fromtimestamp(p.stat().st_mtime) >= cutoff]
|
|
302
278
|
if bad:
|
|
303
279
|
finding("WARN", "cortex", f"{len(bad)} bad model responses in last 24h")
|
|
304
280
|
|
|
305
281
|
|
|
306
|
-
# ── Check 16: Runtime preflight freshness ───────────────────────────────
|
|
307
282
|
def check_runtime_preflight():
|
|
308
283
|
if not RUNTIME_PREFLIGHT_SUMMARY.exists():
|
|
309
|
-
finding("WARN", "preflight", "runtime preflight summary missing")
|
|
310
284
|
return
|
|
311
285
|
data = json.loads(RUNTIME_PREFLIGHT_SUMMARY.read_text())
|
|
312
286
|
ts = data.get("timestamp")
|
|
313
287
|
try:
|
|
314
288
|
when = datetime.fromisoformat(ts)
|
|
315
289
|
except Exception:
|
|
316
|
-
finding("WARN", "preflight", "runtime preflight timestamp invalid")
|
|
317
290
|
return
|
|
318
291
|
if when < datetime.now() - timedelta(days=1):
|
|
319
292
|
finding("WARN", "preflight", "runtime preflight older than 24h")
|
|
@@ -321,17 +294,14 @@ def check_runtime_preflight():
|
|
|
321
294
|
finding("ERROR", "preflight", "runtime preflight failing")
|
|
322
295
|
|
|
323
296
|
|
|
324
|
-
# ── Check 17: Watchdog smoke freshness ──────────────────────────────────
|
|
325
297
|
def check_watchdog_smoke():
|
|
326
298
|
if not WATCHDOG_SMOKE_SUMMARY.exists():
|
|
327
|
-
finding("WARN", "watchdog", "watchdog smoke summary missing")
|
|
328
299
|
return
|
|
329
300
|
data = json.loads(WATCHDOG_SMOKE_SUMMARY.read_text())
|
|
330
301
|
ts = data.get("timestamp")
|
|
331
302
|
try:
|
|
332
303
|
when = datetime.fromisoformat(ts)
|
|
333
304
|
except Exception:
|
|
334
|
-
finding("WARN", "watchdog", "watchdog smoke timestamp invalid")
|
|
335
305
|
return
|
|
336
306
|
if when < datetime.now() - timedelta(days=1):
|
|
337
307
|
finding("WARN", "watchdog", "watchdog smoke older than 24h")
|
|
@@ -339,10 +309,8 @@ def check_watchdog_smoke():
|
|
|
339
309
|
finding("ERROR", "watchdog", "watchdog smoke failing")
|
|
340
310
|
|
|
341
311
|
|
|
342
|
-
# ── Check 18: Cognitive memory health ────────────────────────────────
|
|
343
312
|
def check_cognitive_health():
|
|
344
|
-
|
|
345
|
-
cognitive_db = NEXO_HOME_PATH / "cognitive.db"
|
|
313
|
+
cognitive_db = Path.home() / ".nexo" / "cognitive.db"
|
|
346
314
|
if not cognitive_db.exists():
|
|
347
315
|
finding("WARN", "cognitive", "cognitive.db not found")
|
|
348
316
|
return
|
|
@@ -356,134 +324,150 @@ def check_cognitive_health():
|
|
|
356
324
|
conn.close()
|
|
357
325
|
|
|
358
326
|
size_mb = cognitive_db.stat().st_size / (1024 * 1024)
|
|
359
|
-
finding("INFO", "cognitive", f"STM: {stm_count} (sensory: {sensory_count}) | LTM: {ltm_active} active, {ltm_dormant} dormant | {size_mb:.1f} MB
|
|
327
|
+
finding("INFO", "cognitive", f"STM: {stm_count} (sensory: {sensory_count}) | LTM: {ltm_active} active, {ltm_dormant} dormant | {size_mb:.1f} MB")
|
|
360
328
|
|
|
361
329
|
if avg_stm_str < 0.3 and stm_count > 20:
|
|
362
|
-
finding("WARN", "cognitive", f"STM average strength very low ({avg_stm_str:.2f})
|
|
330
|
+
finding("WARN", "cognitive", f"STM average strength very low ({avg_stm_str:.2f})")
|
|
363
331
|
|
|
364
|
-
# Metrics
|
|
332
|
+
# Metrics
|
|
365
333
|
try:
|
|
366
|
-
sys.path.insert(0, str(
|
|
334
|
+
sys.path.insert(0, str(Path.home() / ".nexo"))
|
|
367
335
|
import cognitive as cog
|
|
368
|
-
|
|
369
336
|
metrics = cog.get_metrics(days=7)
|
|
370
337
|
if metrics["total_retrievals"] > 0:
|
|
371
338
|
finding("INFO", "cognitive-metrics",
|
|
372
|
-
f"7d: {metrics['total_retrievals']} retrievals, "
|
|
373
|
-
f"relevance={metrics['retrieval_relevance_pct']}%, "
|
|
374
|
-
f"avg_score={metrics['avg_top_score']}, "
|
|
375
|
-
f"{metrics['retrievals_per_day']}/day")
|
|
376
|
-
|
|
377
|
-
if metrics["needs_multilingual"]:
|
|
378
|
-
finding("WARN", "cognitive-metrics",
|
|
379
|
-
f"Retrieval relevance {metrics['retrieval_relevance_pct']}% < 70% — consider switching to multilingual model (spec 13.3)")
|
|
380
|
-
|
|
339
|
+
f"7d: {metrics['total_retrievals']} retrievals, relevance={metrics['retrieval_relevance_pct']}%")
|
|
381
340
|
if metrics["retrieval_relevance_pct"] < 50 and metrics["total_retrievals"] >= 5:
|
|
382
|
-
finding("ERROR", "cognitive-metrics",
|
|
383
|
-
f"Retrieval relevance critically low: {metrics['retrieval_relevance_pct']}%")
|
|
341
|
+
finding("ERROR", "cognitive-metrics", f"Relevance critically low: {metrics['retrieval_relevance_pct']}%")
|
|
384
342
|
|
|
385
|
-
# Repeat error rate
|
|
386
343
|
repeats = cog.check_repeat_errors()
|
|
387
|
-
if repeats["new_count"] > 0:
|
|
388
|
-
finding("
|
|
389
|
-
f"Repeat errors: {repeats['duplicate_count']}/{repeats['new_count']} "
|
|
390
|
-
f"({repeats['repeat_rate_pct']}%) — target <10%")
|
|
391
|
-
if repeats["repeat_rate_pct"] > 30:
|
|
392
|
-
finding("WARN", "cognitive-metrics",
|
|
393
|
-
f"Repeat error rate {repeats['repeat_rate_pct']}% exceeds 30% threshold")
|
|
344
|
+
if repeats["new_count"] > 0 and repeats["repeat_rate_pct"] > 30:
|
|
345
|
+
finding("WARN", "cognitive-metrics", f"Repeat rate {repeats['repeat_rate_pct']}% > 30%")
|
|
394
346
|
|
|
395
|
-
#
|
|
347
|
+
# Save metrics
|
|
396
348
|
metrics_file = LOG_DIR / "cognitive-metrics.json"
|
|
397
349
|
metrics_file.write_text(json.dumps({
|
|
398
350
|
"timestamp": datetime.now().isoformat(),
|
|
399
351
|
"retrieval": metrics,
|
|
400
352
|
"repeats": {k: v for k, v in repeats.items() if k != "duplicates"},
|
|
401
353
|
}, indent=2))
|
|
402
|
-
except Exception as e:
|
|
403
|
-
finding("WARN", "cognitive-metrics", f"Metrics collection failed: {e}")
|
|
404
354
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
sys.path.insert(0, str(NEXO_HOME_PATH))
|
|
408
|
-
import cognitive as cog
|
|
409
|
-
|
|
410
|
-
db_cog = cog._get_db()
|
|
411
|
-
|
|
412
|
-
# v2.0: Procedural memory — trigger: >50 procedural change_logs
|
|
413
|
-
procedural_markers = ['1.', '2.', '3.', 'step ', 'Step ', 'then ', 'first ', 'First ', '→', '->', 'SSH', 'scp', 'git commit', 'deploy']
|
|
414
|
-
changes = db_cog.execute('SELECT content FROM ltm_memories WHERE source_type = "change"').fetchall()
|
|
415
|
-
procedural_count = sum(1 for r in changes if sum(1 for m in procedural_markers if m in r[0]) >= 2)
|
|
416
|
-
if procedural_count >= 50:
|
|
417
|
-
finding("WARN", "cognitive-phase", f"v2.0 TRIGGER MET: {procedural_count} procedural memories (>50). Implement Store 4 (memoria procedimental).")
|
|
418
|
-
|
|
419
|
-
# v2.1: MEMORY.md reduction — trigger: RAG relevance >80% for 30 days
|
|
420
|
-
metrics_file = LOG_DIR / "cognitive-metrics-history.json"
|
|
355
|
+
# Track history for phase triggers
|
|
356
|
+
history_file = LOG_DIR / "cognitive-metrics-history.json"
|
|
421
357
|
try:
|
|
422
|
-
history = json.loads(
|
|
358
|
+
history = json.loads(history_file.read_text()) if history_file.exists() else []
|
|
423
359
|
except Exception:
|
|
424
360
|
history = []
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
"date": datetime.now().strftime("%Y-%m-%d"),
|
|
431
|
-
"relevance": m["retrieval_relevance_pct"],
|
|
432
|
-
"retrievals": m["total_retrievals"],
|
|
433
|
-
})
|
|
434
|
-
# Keep last 60 days
|
|
361
|
+
m1 = cog.get_metrics(days=1)
|
|
362
|
+
if m1["total_retrievals"] > 0:
|
|
363
|
+
history.append({"date": datetime.now().strftime("%Y-%m-%d"),
|
|
364
|
+
"relevance": m1["retrieval_relevance_pct"],
|
|
365
|
+
"retrievals": m1["total_retrievals"]})
|
|
435
366
|
history = history[-60:]
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
# Check if last 30 entries all have relevance >80%
|
|
439
|
-
if len(history) >= 30:
|
|
440
|
-
last_30 = history[-30:]
|
|
441
|
-
all_above_80 = all(h["relevance"] >= 80.0 for h in last_30)
|
|
442
|
-
if all_above_80:
|
|
443
|
-
finding("WARN", "cognitive-phase", "v2.1 TRIGGER MET: RAG relevance >80% for 30 consecutive days. Reduce MEMORY.md to ~20 lines.")
|
|
444
|
-
|
|
445
|
-
# v2.2: Dashboard — trigger: 30 days of metrics
|
|
446
|
-
if len(history) >= 30:
|
|
447
|
-
finding("INFO", "cognitive-phase", f"v2.2 TRIGGER MET: {len(history)} days of metrics accumulated. Implement HTML dashboard.")
|
|
448
|
-
|
|
449
|
-
# v3.0: Clustering — trigger: LTM >1000
|
|
450
|
-
ltm_count = db_cog.execute('SELECT COUNT(*) FROM ltm_memories WHERE is_dormant = 0').fetchone()[0]
|
|
451
|
-
if ltm_count >= 1000:
|
|
452
|
-
finding("WARN", "cognitive-phase", f"v3.0 TRIGGER MET: {ltm_count} LTM vectors (>1000). Implement K-means clustering.")
|
|
453
|
-
|
|
454
|
-
# v1.4: Multilingual — already checked in metrics section above
|
|
367
|
+
history_file.write_text(json.dumps(history, indent=2))
|
|
455
368
|
|
|
456
369
|
except Exception as e:
|
|
457
|
-
finding("WARN", "cognitive-
|
|
370
|
+
finding("WARN", "cognitive-metrics", f"Metrics failed: {e}")
|
|
458
371
|
|
|
459
372
|
# Weekly GC on Sundays
|
|
460
373
|
if datetime.now().weekday() == 6:
|
|
461
|
-
log(" Running weekly cognitive GC (Sunday)...")
|
|
462
374
|
try:
|
|
463
|
-
sys.path.insert(0, str(
|
|
375
|
+
sys.path.insert(0, str(Path.home() / ".nexo"))
|
|
464
376
|
import cognitive as cog
|
|
465
|
-
|
|
466
|
-
# 1. Delete STM with strength < 0.1 and > 30 days
|
|
467
377
|
gc_stm = cog.gc_stm()
|
|
468
|
-
|
|
469
|
-
# 2. GC sensory > 48h (should already be cleaned by postmortem, but safety net)
|
|
470
378
|
gc_sensory = cog.gc_sensory(max_age_hours=48)
|
|
471
|
-
|
|
472
|
-
# 3. Delete dormant LTM with strength < 0.1 and > 30 days
|
|
473
379
|
gc_ltm = cog.gc_ltm_dormant(min_age_days=30)
|
|
474
|
-
|
|
475
|
-
log(f" Weekly GC results: STM removed={gc_stm}, sensory removed={gc_sensory}, LTM dormant removed={gc_ltm}")
|
|
476
380
|
if gc_stm + gc_sensory + gc_ltm > 0:
|
|
477
|
-
finding("INFO", "cognitive", f"Weekly GC
|
|
381
|
+
finding("INFO", "cognitive", f"Weekly GC: {gc_stm} STM + {gc_sensory} sensory + {gc_ltm} dormant")
|
|
478
382
|
except Exception as e:
|
|
479
383
|
finding("WARN", "cognitive", f"Weekly GC failed: {e}")
|
|
480
384
|
|
|
481
385
|
|
|
482
|
-
#
|
|
386
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
387
|
+
# Stage B: Interpretation (Claude CLI opus) — NEW in v2
|
|
388
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
389
|
+
|
|
390
|
+
def interpret_findings(raw_findings: list) -> bool:
|
|
391
|
+
"""CLI interprets the raw findings with real understanding."""
|
|
392
|
+
|
|
393
|
+
errors = [f for f in raw_findings if f["severity"] == "ERROR"]
|
|
394
|
+
warns = [f for f in raw_findings if f["severity"] == "WARN"]
|
|
395
|
+
|
|
396
|
+
# Don't invoke CLI if everything is clean
|
|
397
|
+
if not errors and not warns:
|
|
398
|
+
log("Stage B: All clean, no interpretation needed.")
|
|
399
|
+
return True
|
|
400
|
+
|
|
401
|
+
findings_json = json.dumps(raw_findings, ensure_ascii=False, indent=1)
|
|
402
|
+
|
|
403
|
+
prompt = f"""You are NEXO's morning self-audit interpreter. The mechanical checks found
|
|
404
|
+
{len(errors)} errors and {len(warns)} warnings. Your job is to UNDERSTAND what's
|
|
405
|
+
actually wrong, not just list findings.
|
|
406
|
+
|
|
407
|
+
RAW FINDINGS:
|
|
408
|
+
{findings_json}
|
|
409
|
+
|
|
410
|
+
Write an actionable audit report to {LOG_DIR}/self-audit-interpreted.md:
|
|
411
|
+
|
|
412
|
+
# NEXO Self-Audit — {datetime.now().strftime('%Y-%m-%d')}
|
|
413
|
+
|
|
414
|
+
## Critical (needs immediate action)
|
|
415
|
+
[Group related findings, identify ROOT CAUSE, suggest specific fix]
|
|
416
|
+
|
|
417
|
+
## Warnings (should address today)
|
|
418
|
+
[Same: group, root cause, specific action]
|
|
419
|
+
|
|
420
|
+
## Observations
|
|
421
|
+
[Trends, things getting worse, things improving]
|
|
422
|
+
|
|
423
|
+
## Recommended Actions (priority order)
|
|
424
|
+
1. [Most important action with specific command/steps]
|
|
425
|
+
2. ...
|
|
426
|
+
|
|
427
|
+
Be specific. "Fix the DB" is useless. "Archive learnings >90 days in category X
|
|
428
|
+
via sqlite3 nexo.db 'UPDATE...'" is useful.
|
|
429
|
+
|
|
430
|
+
Also write the machine-readable summary to {LOG_DIR}/self-audit-summary.json.
|
|
431
|
+
|
|
432
|
+
Execute without asking."""
|
|
433
|
+
|
|
434
|
+
log("Stage B: Invoking Claude CLI (opus) for interpretation...")
|
|
435
|
+
|
|
436
|
+
env = os.environ.copy()
|
|
437
|
+
env.pop("CLAUDECODE", None)
|
|
438
|
+
env.pop("CLAUDE_CODE", None)
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
result = subprocess.run(
|
|
442
|
+
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
|
|
443
|
+
"--allowedTools", "Read,Write,Edit,Glob,Grep"],
|
|
444
|
+
capture_output=True, text=True, timeout=180, env=env
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if result.returncode != 0:
|
|
448
|
+
log(f"Stage B: CLI error ({result.returncode})")
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
log(f"Stage B: Interpretation complete ({len(result.stdout or '')} chars)")
|
|
452
|
+
return True
|
|
453
|
+
|
|
454
|
+
except subprocess.TimeoutExpired:
|
|
455
|
+
log("Stage B: CLI timed out")
|
|
456
|
+
return False
|
|
457
|
+
except Exception as e:
|
|
458
|
+
log(f"Stage B: {e}")
|
|
459
|
+
return False
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
463
|
+
# Main
|
|
464
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
465
|
+
|
|
483
466
|
def main():
|
|
484
467
|
log("=" * 60)
|
|
485
|
-
log("NEXO Daily Self-Audit starting")
|
|
468
|
+
log("NEXO Daily Self-Audit v2 starting")
|
|
486
469
|
|
|
470
|
+
# Stage A: Run all mechanical checks (unchanged)
|
|
487
471
|
check_overdue_reminders()
|
|
488
472
|
check_overdue_followups()
|
|
489
473
|
check_uncommitted_changes()
|
|
@@ -506,10 +490,9 @@ def main():
|
|
|
506
490
|
errors = sum(1 for f in findings if f["severity"] == "ERROR")
|
|
507
491
|
warns = sum(1 for f in findings if f["severity"] == "WARN")
|
|
508
492
|
infos = sum(1 for f in findings if f["severity"] == "INFO")
|
|
493
|
+
log(f"Stage A complete: {errors} errors, {warns} warnings, {infos} info")
|
|
509
494
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
# Write summary for NEXO startup to read
|
|
495
|
+
# Write raw summary (backward compatible)
|
|
513
496
|
summary_file = LOG_DIR / "self-audit-summary.json"
|
|
514
497
|
summary_file.write_text(json.dumps({
|
|
515
498
|
"timestamp": datetime.now().isoformat(),
|
|
@@ -517,13 +500,15 @@ def main():
|
|
|
517
500
|
"counts": {"error": errors, "warn": warns, "info": infos}
|
|
518
501
|
}, indent=2))
|
|
519
502
|
|
|
520
|
-
#
|
|
503
|
+
# Stage B: CLI interpretation
|
|
504
|
+
interpret_findings(findings)
|
|
505
|
+
|
|
506
|
+
# Register for catch-up
|
|
521
507
|
try:
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
_state_file.write_text(_json.dumps(_state, indent=2))
|
|
508
|
+
state_file = Path.home() / ".nexo" / "operations" / ".catchup-state.json"
|
|
509
|
+
st = json.loads(state_file.read_text()) if state_file.exists() else {}
|
|
510
|
+
st["self-audit"] = datetime.now().isoformat()
|
|
511
|
+
state_file.write_text(json.dumps(st, indent=2))
|
|
527
512
|
except Exception:
|
|
528
513
|
pass
|
|
529
514
|
|