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.
@@ -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 NEXO_HOME/logs/self-audit.log
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
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
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 = NEXO_HOME / "nexo.db"
23
- # Optional: project directory for git checks set via env var
24
- PROJECT_DIR_STR = os.environ.get("NEXO_PROJECT_DIR", "")
25
- PROJECT_DIR = Path(PROJECT_DIR_STR) if PROJECT_DIR_STR else None
26
- HASH_REGISTRY = NEXO_HOME / "scripts" / ".watchdog-hashes"
27
- CORTEX_LOG_DIR = NEXO_HOME / "cortex" / "logs"
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 dir ─────────────────────────
76
+ # ── Check 3: Git uncommitted changes in project repo ───────────────────
76
77
  def check_uncommitted_changes():
77
- if not PROJECT_DIR or not PROJECT_DIR.exists():
78
+ if not PROJECT_REPO_DIR.exists():
78
79
  return
79
80
  result = subprocess.run(
80
81
  ["git", "status", "--porcelain"],
81
- cwd=str(PROJECT_DIR), capture_output=True, text=True
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 project repo")
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 = NEXO_HOME / "cortex" / "evolution-objective.json"
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
- # ── Check 12: Cognitive memory health ────────────────────────────────
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 = NEXO_HOME / "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 (procedural memory).")
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. Consider reducing static memory files.")
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 = NEXO_HOME / "operations" / ".catchup-state.json"
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))