nexo-brain 2.1.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 +7 -7
- package/bin/nexo-brain.js +53 -26
- package/package.json +1 -1
- package/scripts/migrate-to-unified 2.sh +813 -0
- package/scripts/migrate-v1.5-to-v1.6 2.py +778 -0
- package/scripts/migrate-v1.7-to-v1.8 2.py +214 -0
- package/scripts/migrate-v1.7-to-v1.8.py +2 -2
- package/scripts/nexo-preflight.sh +236 -0
- package/scripts/pre-commit-check 2.sh +55 -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-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +159 -0
- package/src/auto_update 2.py +634 -0
- package/src/auto_update.py +25 -0
- package/src/claim_graph 2.py +323 -0
- package/src/cognitive/__init__ 2.py +62 -0
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -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-310.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-310.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-310.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-310.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-310.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/cognitive/_core 2.py +567 -0
- package/src/cognitive/_decay 2.py +382 -0
- package/src/cognitive/_ingest 2.py +892 -0
- package/src/cognitive/_memory 2.py +912 -0
- package/src/cognitive/_search 2.py +949 -0
- package/src/cognitive/_trust 2.py +464 -0
- package/src/cognitive/_trust.py +10 -36
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +106 -0
- package/src/crons/manifest.json +6 -13
- package/src/crons/sync 2.py +217 -0
- package/src/crons/sync.py +151 -6
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +789 -0
- package/src/db/__init__ 2.py +89 -0
- 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__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.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__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.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__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.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__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.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/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +417 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_cron_runs.py +74 -0
- package/src/db/_entities 2.py +178 -0
- package/src/db/_episodic 2.py +738 -0
- package/src/db/_episodic.py +40 -6
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_learnings 2.py +168 -0
- package/src/db/_reminders 2.py +338 -0
- package/src/db/_schema 2.py +364 -0
- package/src/db/_schema.py +64 -0
- package/src/db/_sessions 2.py +300 -0
- package/src/db/_skills.py +514 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/evolution_cycle 2.py +266 -0
- package/src/hnsw_index 2.py +254 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-session.sh +2 -0
- package/src/hooks/capture-tool-logs 2.sh +127 -0
- package/src/hooks/capture-tool-logs.sh +3 -2
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/inbox-hook.sh +3 -2
- package/src/hooks/post-compact 2.sh +148 -0
- package/src/hooks/post-compact.sh +1 -1
- package/src/hooks/pre-compact 2.sh +151 -0
- package/src/hooks/pre-compact.sh +1 -1
- package/src/hooks/session-start 2.sh +268 -0
- package/src/hooks/session-start.sh +6 -3
- package/src/hooks/session-stop 2.sh +140 -0
- package/src/hooks/session-stop.sh +14 -102
- package/src/kg_populate 2.py +290 -0
- package/src/knowledge_graph 2.py +257 -0
- package/src/maintenance 2.py +59 -0
- package/src/migrate_embeddings 2.py +122 -0
- package/src/plugin_loader 2.py +202 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.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/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +805 -0
- package/src/plugins/agents 2.py +52 -0
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +104 -0
- package/src/plugins/cognitive_memory 2.py +564 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +299 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +533 -0
- package/src/plugins/episodic_memory.py +5 -3
- package/src/plugins/evolution 2.py +115 -0
- package/src/plugins/guard 2.py +746 -0
- package/src/plugins/knowledge_graph_tools 2.py +105 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/schedule.py +212 -0
- package/src/plugins/skills.py +264 -0
- package/src/plugins/update 2.py +256 -0
- package/src/requirements 2.txt +12 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -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/check-context 2.py +264 -0
- package/src/scripts/deep-sleep/apply_findings.py +168 -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 +59 -2
- package/src/scripts/deep-sleep/synthesize.py +3 -1
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +242 -0
- package/src/scripts/nexo-catchup.py +65 -29
- package/src/scripts/nexo-cognitive-decay 2.py +182 -0
- package/src/scripts/nexo-cron-wrapper.sh +53 -0
- package/src/scripts/nexo-daily-self-audit 2.py +552 -0
- package/src/scripts/nexo-daily-self-audit.py +4 -2
- package/src/scripts/nexo-deep-sleep 2.sh +97 -0
- package/src/scripts/nexo-deep-sleep.sh +66 -77
- package/src/scripts/nexo-evolution-run 2.py +597 -0
- package/src/scripts/nexo-evolution-run.py +13 -0
- package/src/scripts/nexo-followup-hygiene 2.py +112 -0
- package/src/scripts/nexo-immune 2.py +927 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +245 -0
- package/src/scripts/nexo-learning-housekeep.py +156 -1
- package/src/scripts/nexo-learning-validator 2.py +207 -0
- package/src/scripts/nexo-learning-validator.py +19 -0
- package/src/scripts/nexo-migrate 2.py +232 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +421 -0
- package/src/scripts/nexo-postmortem-consolidator.py +3 -2
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +29 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +345 -0
- package/src/scripts/nexo-reflection 2.py +253 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-send-email 2.py +25 -0
- package/src/scripts/nexo-send-reply 2.py +178 -0
- package/src/scripts/nexo-sleep 2.py +592 -0
- package/src/scripts/nexo-sleep.py +16 -11
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-synthesis 2.py +253 -0
- package/src/scripts/nexo-synthesis.py +46 -3
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +161 -0
- package/src/scripts/nexo-watchdog 2.sh +878 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/nexo-watchdog.sh +72 -19
- package/src/server 2.py +733 -0
- package/src/server.py +11 -2
- package/src/storage_router 2.py +32 -0
- package/src/tools_coordination 2.py +102 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_learnings 2.py +220 -0
- package/src/tools_menu 2.py +227 -0
- package/src/tools_reminders 2.py +86 -0
- package/src/tools_reminders_crud 2.py +159 -0
- package/src/tools_reminders_crud.py +7 -0
- package/src/tools_sessions 2.py +476 -0
- package/src/tools_task_history 2.py +57 -0
- package/templates/CLAUDE.md 2.template +63 -0
- package/templates/openclaw 2.json +13 -0
- package/tests/__init__ 2.py +0 -0
- package/tests/conftest 2.py +71 -0
- package/tests/test_cognitive 2.py +205 -0
- package/tests/test_knowledge_graph 2.py +140 -0
- package/tests/test_migrations 2.py +137 -0
- package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
- /package/src/scripts/{nexo-github-monitor.py → nexo-github-monitor 2.py} +0 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NEXO Learning Validator — Cross-checks findings against existing learnings.
|
|
4
|
+
|
|
5
|
+
Wrapper collects the finding + all learnings from SQLite, then passes
|
|
6
|
+
to Claude CLI (opus) to make an intelligent determination of whether
|
|
7
|
+
the finding is known, related, or genuinely new.
|
|
8
|
+
|
|
9
|
+
Usage as CLI:
|
|
10
|
+
python3 nexo-learning-validator.py "finding text to validate"
|
|
11
|
+
python3 nexo-learning-validator.py --category project "finding text"
|
|
12
|
+
|
|
13
|
+
Usage as library:
|
|
14
|
+
from nexo_learning_validator import validate_finding
|
|
15
|
+
result = validate_finding("CRITICAL: message_id column is NULL")
|
|
16
|
+
if result["known"]:
|
|
17
|
+
print(f"Already known: {result['matching_learnings']}")
|
|
18
|
+
|
|
19
|
+
Exit codes:
|
|
20
|
+
0 = Finding is NEW (not known)
|
|
21
|
+
1 = Finding is KNOWN (matches existing learning)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import sqlite3
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
32
|
+
|
|
33
|
+
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
34
|
+
CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_all_learnings(category: str = None) -> list[dict]:
|
|
38
|
+
"""Fetch all learnings from nexo.db."""
|
|
39
|
+
conn = sqlite3.connect(str(NEXO_DB), timeout=10)
|
|
40
|
+
conn.row_factory = sqlite3.Row
|
|
41
|
+
if category:
|
|
42
|
+
rows = conn.execute(
|
|
43
|
+
"SELECT id, category, title, content FROM learnings WHERE category = ?",
|
|
44
|
+
(category,)
|
|
45
|
+
).fetchall()
|
|
46
|
+
else:
|
|
47
|
+
rows = conn.execute(
|
|
48
|
+
"SELECT id, category, title, content FROM learnings"
|
|
49
|
+
).fetchall()
|
|
50
|
+
conn.close()
|
|
51
|
+
return [dict(r) for r in rows]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_finding(finding: str, category: str = None) -> dict:
|
|
55
|
+
"""
|
|
56
|
+
Validate a finding against existing learnings using Claude CLI.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
{
|
|
60
|
+
"known": bool,
|
|
61
|
+
"confidence": float (0-1),
|
|
62
|
+
"matching_learnings": [{"id": int, "title": str, "similarity": float}],
|
|
63
|
+
"recommendation": str
|
|
64
|
+
}
|
|
65
|
+
"""
|
|
66
|
+
learnings = get_all_learnings(category)
|
|
67
|
+
|
|
68
|
+
if not learnings:
|
|
69
|
+
return {
|
|
70
|
+
"known": False,
|
|
71
|
+
"confidence": 0,
|
|
72
|
+
"matching_learnings": [],
|
|
73
|
+
"recommendation": "No learnings in DB — finding is new by default"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Build compact learnings reference for CLI
|
|
77
|
+
learnings_ref = []
|
|
78
|
+
for l in learnings:
|
|
79
|
+
learnings_ref.append({
|
|
80
|
+
"id": l["id"],
|
|
81
|
+
"cat": l["category"],
|
|
82
|
+
"title": l["title"],
|
|
83
|
+
"content": (l["content"] or "")[:300],
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
prompt = f"""You are a finding deduplication engine. Compare a new finding against existing learnings and determine if it's already known.
|
|
87
|
+
|
|
88
|
+
NEW FINDING:
|
|
89
|
+
{finding}
|
|
90
|
+
|
|
91
|
+
EXISTING LEARNINGS ({len(learnings_ref)} total):
|
|
92
|
+
{json.dumps(learnings_ref, indent=1)}
|
|
93
|
+
|
|
94
|
+
Respond with ONLY valid JSON (no markdown, no code fences):
|
|
95
|
+
{{
|
|
96
|
+
"known": true/false,
|
|
97
|
+
"confidence": 0.0-1.0,
|
|
98
|
+
"matching_learnings": [
|
|
99
|
+
{{"id": <learning_id>, "title": "<title>", "similarity": 0.0-1.0}}
|
|
100
|
+
],
|
|
101
|
+
"recommendation": "<one line: KNOWN/LIKELY KNOWN/POSSIBLY RELATED/NEW>"
|
|
102
|
+
}}
|
|
103
|
+
|
|
104
|
+
Rules:
|
|
105
|
+
- confidence >= 0.7 and same root cause = known: true
|
|
106
|
+
- confidence 0.55-0.7 and related topic = known: true, say LIKELY KNOWN
|
|
107
|
+
- confidence < 0.55 = known: false
|
|
108
|
+
- Max 5 matching_learnings, sorted by similarity descending
|
|
109
|
+
- If the finding describes the SAME bug/issue/pattern as a learning, it's known even if worded differently
|
|
110
|
+
- Be strict: different symptoms of different bugs are NOT the same even if they mention the same file"""
|
|
111
|
+
|
|
112
|
+
# Try CLI first, fall back to mechanical similarity
|
|
113
|
+
if CLAUDE_CLI.exists():
|
|
114
|
+
# Fallback: mechanical SequenceMatcher (original logic)
|
|
115
|
+
return _mechanical_validate(finding, learnings)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _mechanical_validate(finding: str, learnings: list[dict]) -> dict:
|
|
119
|
+
"""Fallback validation using SequenceMatcher when CLI is unavailable."""
|
|
120
|
+
from difflib import SequenceMatcher
|
|
121
|
+
|
|
122
|
+
threshold = 0.45
|
|
123
|
+
finding_kw = _extract_keywords(finding)
|
|
124
|
+
matches = []
|
|
125
|
+
|
|
126
|
+
for learning in learnings:
|
|
127
|
+
title_sim = SequenceMatcher(None, finding.lower(), learning["title"].lower()).ratio()
|
|
128
|
+
content_sim = SequenceMatcher(None, finding.lower(), (learning["content"] or "").lower()).ratio()
|
|
129
|
+
|
|
130
|
+
learning_text = f"{learning['title']} {learning['content'] or ''}"
|
|
131
|
+
learning_kw = _extract_keywords(learning_text)
|
|
132
|
+
kw_overlap = len(finding_kw & learning_kw) / len(finding_kw) if finding_kw and learning_kw else 0
|
|
133
|
+
|
|
134
|
+
combined = max(title_sim, content_sim) * 0.6 + kw_overlap * 0.4
|
|
135
|
+
|
|
136
|
+
if combined >= threshold:
|
|
137
|
+
matches.append({
|
|
138
|
+
"id": learning["id"],
|
|
139
|
+
"category": learning["category"],
|
|
140
|
+
"title": learning["title"],
|
|
141
|
+
"similarity": round(combined, 3),
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
matches.sort(key=lambda x: x["similarity"], reverse=True)
|
|
145
|
+
top = matches[:5]
|
|
146
|
+
|
|
147
|
+
if not top:
|
|
148
|
+
return {"known": False, "confidence": 0, "matching_learnings": [], "recommendation": "NEW finding"}
|
|
149
|
+
|
|
150
|
+
best = top[0]["similarity"]
|
|
151
|
+
if best >= 0.7:
|
|
152
|
+
return {"known": True, "confidence": best, "matching_learnings": top,
|
|
153
|
+
"recommendation": f"KNOWN issue (learning #{top[0]['id']})"}
|
|
154
|
+
elif best >= 0.55:
|
|
155
|
+
return {"known": True, "confidence": best, "matching_learnings": top,
|
|
156
|
+
"recommendation": f"LIKELY KNOWN (learning #{top[0]['id']})"}
|
|
157
|
+
else:
|
|
158
|
+
return {"known": False, "confidence": best, "matching_learnings": top,
|
|
159
|
+
"recommendation": "POSSIBLY RELATED but different enough to report"}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _extract_keywords(text: str) -> set:
|
|
163
|
+
"""Extract meaningful keywords from text."""
|
|
164
|
+
stop_words = {
|
|
165
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
166
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
167
|
+
'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
|
|
168
|
+
'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as',
|
|
169
|
+
'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either',
|
|
170
|
+
'error', 'critical', 'warning', 'bug', 'issue', 'problem', 'fix',
|
|
171
|
+
'el', 'la', 'los', 'las', 'un', 'una', 'de', 'en', 'que', 'por',
|
|
172
|
+
}
|
|
173
|
+
words = set()
|
|
174
|
+
for word in text.lower().split():
|
|
175
|
+
clean = ''.join(c for c in word if c.isalnum() or c == '_')
|
|
176
|
+
if clean and len(clean) > 2 and clean not in stop_words:
|
|
177
|
+
words.add(clean)
|
|
178
|
+
return words
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main():
|
|
182
|
+
import argparse
|
|
183
|
+
parser = argparse.ArgumentParser(description="Validate findings against existing NEXO learnings")
|
|
184
|
+
parser.add_argument("finding", help="The finding text to validate")
|
|
185
|
+
parser.add_argument("--category", "-c", help="Filter learnings by category")
|
|
186
|
+
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
|
187
|
+
args = parser.parse_args()
|
|
188
|
+
|
|
189
|
+
result = validate_finding(args.finding, args.category)
|
|
190
|
+
|
|
191
|
+
if args.json:
|
|
192
|
+
print(json.dumps(result, indent=2))
|
|
193
|
+
else:
|
|
194
|
+
status = "KNOWN" if result["known"] else "NEW"
|
|
195
|
+
print(f"Status: {status} (confidence: {result['confidence']:.0%})")
|
|
196
|
+
print(f"Recommendation: {result['recommendation']}")
|
|
197
|
+
if result["matching_learnings"]:
|
|
198
|
+
print(f"Related learnings:")
|
|
199
|
+
for m in result["matching_learnings"]:
|
|
200
|
+
cat = m.get('category', '?')
|
|
201
|
+
print(f" #{m['id']} [{cat}] {m['title']} ({m['similarity']:.0%})")
|
|
202
|
+
|
|
203
|
+
sys.exit(1 if result["known"] else 0)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
main()
|
|
@@ -111,6 +111,25 @@ Rules:
|
|
|
111
111
|
|
|
112
112
|
# Try CLI first, fall back to mechanical similarity
|
|
113
113
|
if CLAUDE_CLI.exists():
|
|
114
|
+
try:
|
|
115
|
+
env = os.environ.copy()
|
|
116
|
+
env["NEXO_HEADLESS"] = "1"
|
|
117
|
+
env.pop("CLAUDECODE", None)
|
|
118
|
+
env.pop("CLAUDE_CODE", None)
|
|
119
|
+
learnings_text = "\n".join(
|
|
120
|
+
f"[#{l.get('id','')}] {l.get('title','')}: {l.get('content','')[:200]}"
|
|
121
|
+
for l in learnings[:20]
|
|
122
|
+
)
|
|
123
|
+
prompt = f"{VALIDATE_PROMPT}\n\nFinding:\n{finding}\n\nExisting learnings:\n{learnings_text}"
|
|
124
|
+
result = subprocess.run(
|
|
125
|
+
[str(CLAUDE_CLI), "-p", prompt, "--model", "sonnet", "--output-format", "text"],
|
|
126
|
+
capture_output=True, text=True, timeout=60, env=env
|
|
127
|
+
)
|
|
128
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
129
|
+
parsed = json.loads(result.stdout.strip())
|
|
130
|
+
return parsed
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
114
133
|
# Fallback: mechanical SequenceMatcher (original logic)
|
|
115
134
|
return _mechanical_validate(finding, learnings)
|
|
116
135
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""NEXO Migration Tool — automatic, idempotent upgrades between versions.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 nexo-migrate.py # auto-detect current → target
|
|
6
|
+
python3 nexo-migrate.py --dry-run # show what would happen
|
|
7
|
+
python3 nexo-migrate.py --from 1.6.0 # override detected current version
|
|
8
|
+
|
|
9
|
+
Reads current version from $NEXO_HOME/version.json.
|
|
10
|
+
Reads target version from the repo's package.json.
|
|
11
|
+
Backs up NEXO_HOME/db/ before any migration.
|
|
12
|
+
Runs DB schema migrations via the existing _schema.py system.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import sqlite3
|
|
20
|
+
import sys
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", Path.home() / ".nexo"))
|
|
25
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent # nexo/src/scripts -> nexo/
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── Version helpers ──────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
def parse_version(v: str) -> tuple:
|
|
31
|
+
"""Parse '1.7.0-beta.1' → (1, 7, 0, 'beta.1'). Pre-release is optional."""
|
|
32
|
+
parts = v.strip().lstrip("v").split("-", 1)
|
|
33
|
+
nums = tuple(int(x) for x in parts[0].split("."))
|
|
34
|
+
pre = parts[1] if len(parts) > 1 else ""
|
|
35
|
+
return (*nums, pre)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def version_key(v: str) -> tuple:
|
|
39
|
+
"""Sortable key: releases sort after pre-releases of same version."""
|
|
40
|
+
nums = parse_version(v)
|
|
41
|
+
# Empty pre-release string sorts AFTER any pre-release tag
|
|
42
|
+
pre = nums[3] if len(nums) > 3 else ""
|
|
43
|
+
return (nums[0], nums[1], nums[2], 0 if pre else 1, pre)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_current_version() -> str:
|
|
47
|
+
"""Read installed version from NEXO_HOME/version.json."""
|
|
48
|
+
vfile = NEXO_HOME / "version.json"
|
|
49
|
+
if not vfile.exists():
|
|
50
|
+
return "0.0.0"
|
|
51
|
+
try:
|
|
52
|
+
data = json.loads(vfile.read_text())
|
|
53
|
+
return data.get("version", "0.0.0")
|
|
54
|
+
except Exception:
|
|
55
|
+
return "0.0.0"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_target_version() -> str:
|
|
59
|
+
"""Read target version from repo package.json."""
|
|
60
|
+
pkg = REPO_ROOT / "package.json"
|
|
61
|
+
if not pkg.exists():
|
|
62
|
+
print(f"ERROR: package.json not found at {pkg}", file=sys.stderr)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
data = json.loads(pkg.read_text())
|
|
65
|
+
return data["version"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ── Backup ───────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
def backup_databases() -> str:
|
|
71
|
+
"""Backup all .db files before migration. Returns backup dir path."""
|
|
72
|
+
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
|
|
73
|
+
backup_dir = NEXO_HOME / "backups" / f"pre-migrate-{ts}"
|
|
74
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
|
|
76
|
+
data_dir = NEXO_HOME / "data"
|
|
77
|
+
if data_dir.exists():
|
|
78
|
+
for db_file in data_dir.glob("*.db*"):
|
|
79
|
+
shutil.copy2(db_file, backup_dir / db_file.name)
|
|
80
|
+
# Also check legacy db/ location
|
|
81
|
+
legacy_db_dir = NEXO_HOME / "db"
|
|
82
|
+
if legacy_db_dir.exists():
|
|
83
|
+
for db_file in legacy_db_dir.glob("*.db*"):
|
|
84
|
+
if not (backup_dir / db_file.name).exists():
|
|
85
|
+
shutil.copy2(db_file, backup_dir / db_file.name)
|
|
86
|
+
|
|
87
|
+
# Also backup version.json
|
|
88
|
+
vfile = NEXO_HOME / "version.json"
|
|
89
|
+
if vfile.exists():
|
|
90
|
+
shutil.copy2(vfile, backup_dir / "version.json")
|
|
91
|
+
|
|
92
|
+
return str(backup_dir)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ── Migration steps ──────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
def ensure_nexo_home_dirs():
|
|
98
|
+
"""Create all required NEXO_HOME subdirectories."""
|
|
99
|
+
dirs = [
|
|
100
|
+
"db", "brain", "logs", "operations", "coordination",
|
|
101
|
+
"scripts", "hooks", "plugins", "backups", "memory",
|
|
102
|
+
"docs", "projects", "learnings", "agents", "skills",
|
|
103
|
+
]
|
|
104
|
+
for d in dirs:
|
|
105
|
+
(NEXO_HOME / d).mkdir(parents=True, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def run_db_schema_migrations():
|
|
109
|
+
"""Run the formal DB schema migration system from _schema.py."""
|
|
110
|
+
# Add src/ to path so we can import the db module
|
|
111
|
+
src_dir = REPO_ROOT / "src"
|
|
112
|
+
if str(src_dir) not in sys.path:
|
|
113
|
+
sys.path.insert(0, str(src_dir))
|
|
114
|
+
|
|
115
|
+
# Set NEXO_HOME env for the db module
|
|
116
|
+
os.environ["NEXO_HOME"] = str(NEXO_HOME)
|
|
117
|
+
os.environ["NEXO_SKIP_FS_INDEX"] = "1" # Don't rebuild FTS during migration
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
from db import init_db
|
|
121
|
+
init_db()
|
|
122
|
+
print(" DB schema migrations applied.")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print(f" WARNING: DB schema migration error: {e}", file=sys.stderr)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def write_version_json(version: str):
|
|
128
|
+
"""Write version.json with the installed version."""
|
|
129
|
+
vfile = NEXO_HOME / "version.json"
|
|
130
|
+
data = {
|
|
131
|
+
"version": version,
|
|
132
|
+
"installed_at": datetime.now().isoformat(timespec="seconds"),
|
|
133
|
+
"nexo_home": str(NEXO_HOME),
|
|
134
|
+
}
|
|
135
|
+
vfile.write_text(json.dumps(data, indent=2) + "\n")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ── Migration registry ───────────────────────────────────────────
|
|
139
|
+
# Each entry: version → list of (description, callable)
|
|
140
|
+
# Migrations run for all versions > current AND <= target.
|
|
141
|
+
|
|
142
|
+
def _migrate_1_7_0():
|
|
143
|
+
"""1.7.0: Ensure NEXO_HOME paths, create directories, update version."""
|
|
144
|
+
ensure_nexo_home_dirs()
|
|
145
|
+
run_db_schema_migrations()
|
|
146
|
+
print(" Created/verified all NEXO_HOME directories.")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
MIGRATION_REGISTRY: dict[str, list[tuple[str, callable]]] = {
|
|
150
|
+
"1.7.0": [
|
|
151
|
+
("Ensure NEXO_HOME dirs + DB schema", _migrate_1_7_0),
|
|
152
|
+
],
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── Main ─────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
def get_applicable_migrations(current: str, target: str) -> list[tuple[str, str, callable]]:
|
|
159
|
+
"""Return list of (version, description, fn) for migrations between current and target."""
|
|
160
|
+
current_key = version_key(current)
|
|
161
|
+
target_key = version_key(target)
|
|
162
|
+
|
|
163
|
+
applicable = []
|
|
164
|
+
for ver, steps in sorted(MIGRATION_REGISTRY.items(), key=lambda x: version_key(x[0])):
|
|
165
|
+
ver_key = version_key(ver)
|
|
166
|
+
# Run if version > current and <= target (base version comparison)
|
|
167
|
+
base_ver = ver.split("-")[0] # strip pre-release for comparison
|
|
168
|
+
base_ver_key = version_key(base_ver)
|
|
169
|
+
if base_ver_key > (current_key[0], current_key[1], current_key[2], current_key[3], current_key[4] if len(current_key) > 4 else ""):
|
|
170
|
+
if base_ver_key <= (target_key[0], target_key[1], target_key[2], 1, ""):
|
|
171
|
+
for desc, fn in steps:
|
|
172
|
+
applicable.append((ver, desc, fn))
|
|
173
|
+
|
|
174
|
+
return applicable
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main():
|
|
178
|
+
parser = argparse.ArgumentParser(description="NEXO Migration Tool")
|
|
179
|
+
parser.add_argument("--dry-run", action="store_true", help="Show what would happen without executing")
|
|
180
|
+
parser.add_argument("--from", dest="from_ver", help="Override detected current version")
|
|
181
|
+
args = parser.parse_args()
|
|
182
|
+
|
|
183
|
+
current = args.from_ver or get_current_version()
|
|
184
|
+
target = get_target_version()
|
|
185
|
+
|
|
186
|
+
print(f"NEXO Migration: {current} → {target}")
|
|
187
|
+
print(f"NEXO_HOME: {NEXO_HOME}")
|
|
188
|
+
print()
|
|
189
|
+
|
|
190
|
+
if version_key(current) >= version_key(target):
|
|
191
|
+
print("Already up to date. Nothing to migrate.")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
migrations = get_applicable_migrations(current, target)
|
|
195
|
+
if not migrations:
|
|
196
|
+
print("No migration steps needed (only version bump).")
|
|
197
|
+
else:
|
|
198
|
+
print(f"Migrations to run ({len(migrations)}):")
|
|
199
|
+
for ver, desc, _ in migrations:
|
|
200
|
+
print(f" [{ver}] {desc}")
|
|
201
|
+
print()
|
|
202
|
+
|
|
203
|
+
if args.dry_run:
|
|
204
|
+
print("DRY RUN — no changes made.")
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
# Backup before anything
|
|
208
|
+
backup_path = backup_databases()
|
|
209
|
+
print(f"Backup created: {backup_path}")
|
|
210
|
+
print()
|
|
211
|
+
|
|
212
|
+
# Ensure base directories exist
|
|
213
|
+
ensure_nexo_home_dirs()
|
|
214
|
+
|
|
215
|
+
# Run migrations
|
|
216
|
+
for ver, desc, fn in migrations:
|
|
217
|
+
print(f"Running [{ver}] {desc}...")
|
|
218
|
+
try:
|
|
219
|
+
fn()
|
|
220
|
+
print(f" Done.")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
print(f" ERROR: {e}", file=sys.stderr)
|
|
223
|
+
print(f" Backup at: {backup_path}", file=sys.stderr)
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
|
|
226
|
+
# Write final version
|
|
227
|
+
write_version_json(target)
|
|
228
|
+
print(f"\nMigration complete: {current} → {target}")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
main()
|