nexo-brain 0.2.1 → 0.3.2
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 +158 -72
- package/bin/nexo-brain 2.js +610 -0
- package/package.json +2 -2
- package/scripts/pre-commit-check 2.sh +55 -0
- package/src/cognitive.py +1582 -56
- package/src/db.py +49 -25
- package/src/hooks/auto_capture.py +208 -0
- package/src/plugins/cognitive_memory.py +276 -17
- package/src/scripts/nexo-catchup.py +32 -15
- package/src/scripts/nexo-cognitive-decay.py +2 -4
- package/src/scripts/nexo-daily-self-audit.py +148 -29
- package/src/scripts/nexo-immune.py +869 -0
- package/src/scripts/nexo-postmortem-consolidator.py +42 -40
- package/src/scripts/nexo-sleep.py +90 -39
- package/src/scripts/nexo-synthesis.py +78 -76
- package/src/tools_sessions.py +2 -2
- package/templates/CLAUDE.md 2.template +89 -0
- package/templates/CLAUDE.md.template +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
NEXO Daily Self-Audit
|
|
4
4
|
Proactively scans for common issues before they become problems.
|
|
5
|
-
Runs via launchd at 7:00 AM daily. Results saved to
|
|
5
|
+
Runs via launchd at 7:00 AM daily. Results saved to ~/claude/logs/self-audit.log
|
|
6
6
|
"""
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
@@ -14,17 +14,18 @@ import hashlib
|
|
|
14
14
|
from datetime import datetime, timedelta
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
LOG_DIR = NEXO_HOME / "logs"
|
|
17
|
+
LOG_DIR = Path.home() / "claude" / "logs"
|
|
20
18
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
21
19
|
LOG_FILE = LOG_DIR / "self-audit.log"
|
|
22
|
-
NEXO_DB =
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
NEXO_DB = Path.home() / "claude" / "nexo-mcp" / "nexo.db"
|
|
21
|
+
# Configure this to point to your main project repo for uncommitted-changes check
|
|
22
|
+
PROJECT_REPO_DIR = Path(os.environ.get("NEXO_PROJECT_REPO", str(Path.home() / "projects" / "main")))
|
|
23
|
+
HASH_REGISTRY = Path.home() / "claude" / "scripts" / ".watchdog-hashes"
|
|
24
|
+
SNAPSHOT_GOLDEN = Path.home() / "claude" / "snapshots" / "golden" / "files" / "claude"
|
|
25
|
+
RUNTIME_PREFLIGHT_SUMMARY = LOG_DIR / "runtime-preflight-summary.json"
|
|
26
|
+
WATCHDOG_SMOKE_SUMMARY = LOG_DIR / "watchdog-smoke-summary.json"
|
|
27
|
+
RESTORE_LOG = LOG_DIR / "snapshot-restores.log"
|
|
28
|
+
CORTEX_LOG_DIR = Path.home() / "claude" / "cortex" / "logs"
|
|
28
29
|
|
|
29
30
|
findings = []
|
|
30
31
|
|
|
@@ -72,17 +73,17 @@ def check_overdue_followups():
|
|
|
72
73
|
finding("WARN", "followups", f"{len(rows)} overdue: {', '.join(r[0][:40] for r in rows[:5])}")
|
|
73
74
|
|
|
74
75
|
|
|
75
|
-
# ── Check 3: Git uncommitted changes in project
|
|
76
|
+
# ── Check 3: Git uncommitted changes in project repo ───────────────────
|
|
76
77
|
def check_uncommitted_changes():
|
|
77
|
-
if not
|
|
78
|
+
if not PROJECT_REPO_DIR.exists():
|
|
78
79
|
return
|
|
79
80
|
result = subprocess.run(
|
|
80
81
|
["git", "status", "--porcelain"],
|
|
81
|
-
cwd=str(
|
|
82
|
+
cwd=str(PROJECT_REPO_DIR), capture_output=True, text=True
|
|
82
83
|
)
|
|
83
84
|
lines = [l for l in result.stdout.strip().split("\n") if l.strip()]
|
|
84
85
|
if len(lines) > 10:
|
|
85
|
-
finding("WARN", "git", f"{len(lines)} uncommitted changes in
|
|
86
|
+
finding("WARN", "git", f"{len(lines)} uncommitted changes in {PROJECT_REPO_DIR.name} repo")
|
|
86
87
|
|
|
87
88
|
|
|
88
89
|
# ── Check 4: Cron error logs (last 24h) ────────────────────────────────
|
|
@@ -102,7 +103,7 @@ def check_cron_errors():
|
|
|
102
103
|
|
|
103
104
|
# ── Check 5: Evolution failures ─────────────────────────────────────────
|
|
104
105
|
def check_evolution_health():
|
|
105
|
-
obj_file =
|
|
106
|
+
obj_file = Path.home() / "claude" / "cortex" / "evolution-objective.json"
|
|
106
107
|
if not obj_file.exists():
|
|
107
108
|
return
|
|
108
109
|
obj = json.loads(obj_file.read_text())
|
|
@@ -225,10 +226,122 @@ def check_memory_reviews():
|
|
|
225
226
|
finding("INFO", "memory", f"{total_due} memory reviews due ({due_decisions} decisions, {due_learnings} learnings)")
|
|
226
227
|
|
|
227
228
|
|
|
228
|
-
|
|
229
|
+
def _sha256(path: Path) -> str:
|
|
230
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ── Check 12: Watchdog registry sanity ──────────────────────────────────
|
|
234
|
+
def check_watchdog_registry():
|
|
235
|
+
if not HASH_REGISTRY.exists():
|
|
236
|
+
finding("WARN", "watchdog", "hash registry missing")
|
|
237
|
+
return
|
|
238
|
+
text = HASH_REGISTRY.read_text(errors="ignore")
|
|
239
|
+
forbidden = ["CLAUDE.md", "db.py", "server.py", "plugin_loader.py", "cortex-wrapper.py"]
|
|
240
|
+
bad = [name for name in forbidden if name in text]
|
|
241
|
+
if bad:
|
|
242
|
+
finding("ERROR", "watchdog", f"mutable files still protected by watchdog: {', '.join(bad)}")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# ── Check 13: Snapshot drift on protected recovery files ────────────────
|
|
246
|
+
def check_snapshot_sync():
|
|
247
|
+
pairs = [
|
|
248
|
+
(Path.home() / "claude" / "nexo-mcp" / "db.py", SNAPSHOT_GOLDEN / "nexo-mcp" / "db.py"),
|
|
249
|
+
(Path.home() / "claude" / "cortex" / "cortex-wrapper.py", SNAPSHOT_GOLDEN / "cortex" / "cortex-wrapper.py"),
|
|
250
|
+
(Path.home() / "claude" / "cortex" / "evolution_cycle.py", SNAPSHOT_GOLDEN / "cortex" / "evolution_cycle.py"),
|
|
251
|
+
]
|
|
252
|
+
drift = []
|
|
253
|
+
for live, snap in pairs:
|
|
254
|
+
if not live.exists() or not snap.exists():
|
|
255
|
+
drift.append(live.name)
|
|
256
|
+
continue
|
|
257
|
+
if _sha256(live) != _sha256(snap):
|
|
258
|
+
drift.append(live.name)
|
|
259
|
+
if drift:
|
|
260
|
+
finding("WARN", "snapshots", f"golden snapshot drift: {', '.join(drift)}")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ── Check 14: Recent restore activity ───────────────────────────────────
|
|
264
|
+
def check_restore_activity():
|
|
265
|
+
if not RESTORE_LOG.exists():
|
|
266
|
+
return
|
|
267
|
+
cutoff_day = datetime.now() - timedelta(days=1)
|
|
268
|
+
current_hour_prefix = datetime.now().strftime("%Y-%m-%d %H")
|
|
269
|
+
recent_day = 0
|
|
270
|
+
recent_hour = 0
|
|
271
|
+
for line in RESTORE_LOG.read_text(errors="ignore").splitlines():
|
|
272
|
+
if not line.startswith("["):
|
|
273
|
+
continue
|
|
274
|
+
if "/.codex/memories/nexo-" in line:
|
|
275
|
+
continue
|
|
276
|
+
try:
|
|
277
|
+
ts = datetime.strptime(line[1:20], "%Y-%m-%d %H:%M:%S")
|
|
278
|
+
except ValueError:
|
|
279
|
+
continue
|
|
280
|
+
if ts >= cutoff_day:
|
|
281
|
+
recent_day += 1
|
|
282
|
+
if line[1:14] == current_hour_prefix:
|
|
283
|
+
recent_hour += 1
|
|
284
|
+
if recent_hour > 2:
|
|
285
|
+
finding("ERROR", "restore", f"{recent_hour} snapshot restores in last hour")
|
|
286
|
+
elif recent_day > 5:
|
|
287
|
+
finding("WARN", "restore", f"{recent_day} snapshot restores in last 24h")
|
|
288
|
+
elif recent_day > 0:
|
|
289
|
+
finding("INFO", "restore", f"{recent_day} snapshot restores in last 24h (historical activity)")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ── Check 15: Bad model responses ───────────────────────────────────────
|
|
293
|
+
def check_bad_responses():
|
|
294
|
+
if not CORTEX_LOG_DIR.exists():
|
|
295
|
+
return
|
|
296
|
+
cutoff = datetime.now() - timedelta(days=1)
|
|
297
|
+
bad = [
|
|
298
|
+
p for p in CORTEX_LOG_DIR.glob("bad-response-*.json")
|
|
299
|
+
if datetime.fromtimestamp(p.stat().st_mtime) >= cutoff
|
|
300
|
+
]
|
|
301
|
+
if bad:
|
|
302
|
+
finding("WARN", "cortex", f"{len(bad)} bad model responses in last 24h")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ── Check 16: Runtime preflight freshness ───────────────────────────────
|
|
306
|
+
def check_runtime_preflight():
|
|
307
|
+
if not RUNTIME_PREFLIGHT_SUMMARY.exists():
|
|
308
|
+
finding("WARN", "preflight", "runtime preflight summary missing")
|
|
309
|
+
return
|
|
310
|
+
data = json.loads(RUNTIME_PREFLIGHT_SUMMARY.read_text())
|
|
311
|
+
ts = data.get("timestamp")
|
|
312
|
+
try:
|
|
313
|
+
when = datetime.fromisoformat(ts)
|
|
314
|
+
except Exception:
|
|
315
|
+
finding("WARN", "preflight", "runtime preflight timestamp invalid")
|
|
316
|
+
return
|
|
317
|
+
if when < datetime.now() - timedelta(days=1):
|
|
318
|
+
finding("WARN", "preflight", "runtime preflight older than 24h")
|
|
319
|
+
if not data.get("ok", False):
|
|
320
|
+
finding("ERROR", "preflight", "runtime preflight failing")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ── Check 17: Watchdog smoke freshness ──────────────────────────────────
|
|
324
|
+
def check_watchdog_smoke():
|
|
325
|
+
if not WATCHDOG_SMOKE_SUMMARY.exists():
|
|
326
|
+
finding("WARN", "watchdog", "watchdog smoke summary missing")
|
|
327
|
+
return
|
|
328
|
+
data = json.loads(WATCHDOG_SMOKE_SUMMARY.read_text())
|
|
329
|
+
ts = data.get("timestamp")
|
|
330
|
+
try:
|
|
331
|
+
when = datetime.fromisoformat(ts)
|
|
332
|
+
except Exception:
|
|
333
|
+
finding("WARN", "watchdog", "watchdog smoke timestamp invalid")
|
|
334
|
+
return
|
|
335
|
+
if when < datetime.now() - timedelta(days=1):
|
|
336
|
+
finding("WARN", "watchdog", "watchdog smoke older than 24h")
|
|
337
|
+
if not data.get("ok", False):
|
|
338
|
+
finding("ERROR", "watchdog", "watchdog smoke failing")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ── Check 18: Cognitive memory health ────────────────────────────────
|
|
229
342
|
def check_cognitive_health():
|
|
230
343
|
"""Check cognitive.db health and run weekly GC on Sundays."""
|
|
231
|
-
cognitive_db =
|
|
344
|
+
cognitive_db = Path.home() / "claude" / "nexo-mcp" / "cognitive.db"
|
|
232
345
|
if not cognitive_db.exists():
|
|
233
346
|
finding("WARN", "cognitive", "cognitive.db not found")
|
|
234
347
|
return
|
|
@@ -249,6 +362,7 @@ def check_cognitive_health():
|
|
|
249
362
|
|
|
250
363
|
# Metrics report (spec section 9)
|
|
251
364
|
try:
|
|
365
|
+
sys.path.insert(0, str(Path.home() / "claude" / "nexo-mcp"))
|
|
252
366
|
import cognitive as cog
|
|
253
367
|
|
|
254
368
|
metrics = cog.get_metrics(days=7)
|
|
@@ -261,7 +375,7 @@ def check_cognitive_health():
|
|
|
261
375
|
|
|
262
376
|
if metrics["needs_multilingual"]:
|
|
263
377
|
finding("WARN", "cognitive-metrics",
|
|
264
|
-
f"Retrieval relevance {metrics['retrieval_relevance_pct']}% < 70% — consider switching to multilingual model")
|
|
378
|
+
f"Retrieval relevance {metrics['retrieval_relevance_pct']}% < 70% — consider switching to multilingual model (spec 13.3)")
|
|
265
379
|
|
|
266
380
|
if metrics["retrieval_relevance_pct"] < 50 and metrics["total_retrievals"] >= 5:
|
|
267
381
|
finding("ERROR", "cognitive-metrics",
|
|
@@ -287,20 +401,21 @@ def check_cognitive_health():
|
|
|
287
401
|
except Exception as e:
|
|
288
402
|
finding("WARN", "cognitive-metrics", f"Metrics collection failed: {e}")
|
|
289
403
|
|
|
290
|
-
# Phase triggers monitoring
|
|
404
|
+
# Phase triggers monitoring (spec section 10)
|
|
291
405
|
try:
|
|
406
|
+
sys.path.insert(0, str(Path.home() / "claude" / "nexo-mcp"))
|
|
292
407
|
import cognitive as cog
|
|
293
408
|
|
|
294
409
|
db_cog = cog._get_db()
|
|
295
410
|
|
|
296
411
|
# v2.0: Procedural memory — trigger: >50 procedural change_logs
|
|
297
|
-
procedural_markers = ['1.', '2.', '3.', 'step ', 'Step ', 'then ', 'first ', 'First ', '→', '->', 'git commit', 'deploy']
|
|
412
|
+
procedural_markers = ['1.', '2.', '3.', 'step ', 'Step ', 'then ', 'first ', 'First ', '→', '->', 'SSH', 'scp', 'git commit', 'deploy']
|
|
298
413
|
changes = db_cog.execute('SELECT content FROM ltm_memories WHERE source_type = "change"').fetchall()
|
|
299
414
|
procedural_count = sum(1 for r in changes if sum(1 for m in procedural_markers if m in r[0]) >= 2)
|
|
300
415
|
if procedural_count >= 50:
|
|
301
|
-
finding("WARN", "cognitive-phase", f"v2.0 TRIGGER MET: {procedural_count} procedural memories (>50). Implement Store 4 (
|
|
416
|
+
finding("WARN", "cognitive-phase", f"v2.0 TRIGGER MET: {procedural_count} procedural memories (>50). Implement Store 4 (memoria procedimental).")
|
|
302
417
|
|
|
303
|
-
# v2.1: MEMORY reduction — trigger: RAG relevance >80% for 30 days
|
|
418
|
+
# v2.1: MEMORY.md reduction — trigger: RAG relevance >80% for 30 days
|
|
304
419
|
metrics_file = LOG_DIR / "cognitive-metrics-history.json"
|
|
305
420
|
try:
|
|
306
421
|
history = json.loads(metrics_file.read_text()) if metrics_file.exists() else []
|
|
@@ -324,7 +439,7 @@ def check_cognitive_health():
|
|
|
324
439
|
last_30 = history[-30:]
|
|
325
440
|
all_above_80 = all(h["relevance"] >= 80.0 for h in last_30)
|
|
326
441
|
if all_above_80:
|
|
327
|
-
finding("WARN", "cognitive-phase", "v2.1 TRIGGER MET: RAG relevance >80% for 30 consecutive days.
|
|
442
|
+
finding("WARN", "cognitive-phase", "v2.1 TRIGGER MET: RAG relevance >80% for 30 consecutive days. Reduce MEMORY.md to ~20 lines.")
|
|
328
443
|
|
|
329
444
|
# v2.2: Dashboard — trigger: 30 days of metrics
|
|
330
445
|
if len(history) >= 30:
|
|
@@ -335,6 +450,8 @@ def check_cognitive_health():
|
|
|
335
450
|
if ltm_count >= 1000:
|
|
336
451
|
finding("WARN", "cognitive-phase", f"v3.0 TRIGGER MET: {ltm_count} LTM vectors (>1000). Implement K-means clustering.")
|
|
337
452
|
|
|
453
|
+
# v1.4: Multilingual — already checked in metrics section above
|
|
454
|
+
|
|
338
455
|
except Exception as e:
|
|
339
456
|
finding("WARN", "cognitive-phase", f"Phase trigger check failed: {e}")
|
|
340
457
|
|
|
@@ -342,6 +459,7 @@ def check_cognitive_health():
|
|
|
342
459
|
if datetime.now().weekday() == 6:
|
|
343
460
|
log(" Running weekly cognitive GC (Sunday)...")
|
|
344
461
|
try:
|
|
462
|
+
sys.path.insert(0, str(Path.home() / "claude" / "nexo-mcp"))
|
|
345
463
|
import cognitive as cog
|
|
346
464
|
|
|
347
465
|
# 1. Delete STM with strength < 0.1 and > 30 days
|
|
@@ -362,11 +480,6 @@ def check_cognitive_health():
|
|
|
362
480
|
|
|
363
481
|
# ── Main ────────────────────────────────────────────────────────────────
|
|
364
482
|
def main():
|
|
365
|
-
# Ensure cognitive module is importable
|
|
366
|
-
src_dir = NEXO_HOME / "src"
|
|
367
|
-
if src_dir.exists() and str(src_dir) not in sys.path:
|
|
368
|
-
sys.path.insert(0, str(src_dir))
|
|
369
|
-
|
|
370
483
|
log("=" * 60)
|
|
371
484
|
log("NEXO Daily Self-Audit starting")
|
|
372
485
|
|
|
@@ -381,6 +494,12 @@ def main():
|
|
|
381
494
|
check_repetition_rate()
|
|
382
495
|
check_unused_learnings()
|
|
383
496
|
check_memory_reviews()
|
|
497
|
+
check_watchdog_registry()
|
|
498
|
+
check_snapshot_sync()
|
|
499
|
+
check_restore_activity()
|
|
500
|
+
check_bad_responses()
|
|
501
|
+
check_runtime_preflight()
|
|
502
|
+
check_watchdog_smoke()
|
|
384
503
|
check_cognitive_health()
|
|
385
504
|
|
|
386
505
|
errors = sum(1 for f in findings if f["severity"] == "ERROR")
|
|
@@ -400,7 +519,7 @@ def main():
|
|
|
400
519
|
# Register successful run for catch-up
|
|
401
520
|
try:
|
|
402
521
|
import json as _json
|
|
403
|
-
_state_file =
|
|
522
|
+
_state_file = Path.home() / "claude" / "operations" / ".catchup-state.json"
|
|
404
523
|
_state = _json.loads(_state_file.read_text()) if _state_file.exists() else {}
|
|
405
524
|
_state["self-audit"] = datetime.now().isoformat()
|
|
406
525
|
_state_file.write_text(_json.dumps(_state, indent=2))
|