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.
Files changed (297) hide show
  1. package/README.md +7 -7
  2. package/bin/nexo-brain.js +53 -26
  3. package/package.json +1 -1
  4. package/scripts/migrate-to-unified 2.sh +813 -0
  5. package/scripts/migrate-v1.5-to-v1.6 2.py +778 -0
  6. package/scripts/migrate-v1.7-to-v1.8 2.py +214 -0
  7. package/scripts/migrate-v1.7-to-v1.8.py +2 -2
  8. package/scripts/nexo-preflight.sh +236 -0
  9. package/scripts/pre-commit-check 2.sh +55 -0
  10. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  11. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  12. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  13. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  14. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  15. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  16. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  17. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  18. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  19. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  20. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  21. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  22. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  23. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  24. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  25. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  26. package/src/auto_close_sessions 2.py +159 -0
  27. package/src/auto_update 2.py +634 -0
  28. package/src/auto_update.py +25 -0
  29. package/src/claim_graph 2.py +323 -0
  30. package/src/cognitive/__init__ 2.py +62 -0
  31. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  32. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  33. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  34. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  35. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  36. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  37. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  38. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  39. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  40. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  41. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  42. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  43. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  44. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  45. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  46. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  47. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  48. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  49. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  50. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  51. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  52. package/src/cognitive/_core 2.py +567 -0
  53. package/src/cognitive/_decay 2.py +382 -0
  54. package/src/cognitive/_ingest 2.py +892 -0
  55. package/src/cognitive/_memory 2.py +912 -0
  56. package/src/cognitive/_search 2.py +949 -0
  57. package/src/cognitive/_trust 2.py +464 -0
  58. package/src/cognitive/_trust.py +10 -36
  59. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  60. package/src/crons/manifest 2.json +106 -0
  61. package/src/crons/manifest.json +6 -13
  62. package/src/crons/sync 2.py +217 -0
  63. package/src/crons/sync.py +151 -6
  64. package/src/dashboard/__init__ 2.py +0 -0
  65. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  66. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  67. package/src/dashboard/app 2.py +789 -0
  68. package/src/db/__init__ 2.py +89 -0
  69. package/src/db/__init__.py +13 -0
  70. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  71. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  72. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  73. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  74. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  75. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  76. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  77. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  78. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  79. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  80. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  81. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  82. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  83. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  84. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  85. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  86. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  87. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  111. package/src/db/_core 2.py +417 -0
  112. package/src/db/_credentials 2.py +124 -0
  113. package/src/db/_cron_runs.py +74 -0
  114. package/src/db/_entities 2.py +178 -0
  115. package/src/db/_episodic 2.py +738 -0
  116. package/src/db/_episodic.py +40 -6
  117. package/src/db/_evolution 2.py +54 -0
  118. package/src/db/_fts 2.py +406 -0
  119. package/src/db/_learnings 2.py +168 -0
  120. package/src/db/_reminders 2.py +338 -0
  121. package/src/db/_schema 2.py +364 -0
  122. package/src/db/_schema.py +64 -0
  123. package/src/db/_sessions 2.py +300 -0
  124. package/src/db/_skills.py +514 -0
  125. package/src/db/_tasks 2.py +91 -0
  126. package/src/evolution_cycle 2.py +266 -0
  127. package/src/hnsw_index 2.py +254 -0
  128. package/src/hooks/auto_capture 2.py +208 -0
  129. package/src/hooks/caffeinate-guard 2.sh +8 -0
  130. package/src/hooks/capture-session 2.sh +21 -0
  131. package/src/hooks/capture-session.sh +2 -0
  132. package/src/hooks/capture-tool-logs 2.sh +127 -0
  133. package/src/hooks/capture-tool-logs.sh +3 -2
  134. package/src/hooks/daily-briefing-check 2.sh +33 -0
  135. package/src/hooks/inbox-hook 2.sh +76 -0
  136. package/src/hooks/inbox-hook.sh +3 -2
  137. package/src/hooks/post-compact 2.sh +148 -0
  138. package/src/hooks/post-compact.sh +1 -1
  139. package/src/hooks/pre-compact 2.sh +151 -0
  140. package/src/hooks/pre-compact.sh +1 -1
  141. package/src/hooks/session-start 2.sh +268 -0
  142. package/src/hooks/session-start.sh +6 -3
  143. package/src/hooks/session-stop 2.sh +140 -0
  144. package/src/hooks/session-stop.sh +14 -102
  145. package/src/kg_populate 2.py +290 -0
  146. package/src/knowledge_graph 2.py +257 -0
  147. package/src/maintenance 2.py +59 -0
  148. package/src/migrate_embeddings 2.py +122 -0
  149. package/src/plugin_loader 2.py +202 -0
  150. package/src/plugins/__init__ 2.py +0 -0
  151. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  152. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  155. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  156. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  157. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  183. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  185. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  187. package/src/plugins/adaptive_mode 2.py +805 -0
  188. package/src/plugins/agents 2.py +52 -0
  189. package/src/plugins/artifact_registry 2.py +450 -0
  190. package/src/plugins/backup 2.py +104 -0
  191. package/src/plugins/cognitive_memory 2.py +564 -0
  192. package/src/plugins/core_rules 2.py +252 -0
  193. package/src/plugins/cortex 2.py +299 -0
  194. package/src/plugins/entities 2.py +67 -0
  195. package/src/plugins/episodic_memory 2.py +533 -0
  196. package/src/plugins/episodic_memory.py +5 -3
  197. package/src/plugins/evolution 2.py +115 -0
  198. package/src/plugins/guard 2.py +746 -0
  199. package/src/plugins/knowledge_graph_tools 2.py +105 -0
  200. package/src/plugins/preferences 2.py +47 -0
  201. package/src/plugins/schedule.py +212 -0
  202. package/src/plugins/skills.py +264 -0
  203. package/src/plugins/update 2.py +256 -0
  204. package/src/requirements 2.txt +12 -0
  205. package/src/rules/__init__ 2.py +0 -0
  206. package/src/rules/core-rules 2.json +331 -0
  207. package/src/rules/migrate 2.py +207 -0
  208. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  209. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  210. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  211. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  216. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  217. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  218. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  219. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  220. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  221. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  222. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  223. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  229. package/src/scripts/check-context 2.py +264 -0
  230. package/src/scripts/deep-sleep/apply_findings.py +168 -8
  231. package/src/scripts/deep-sleep/collect.py +33 -11
  232. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  233. package/src/scripts/deep-sleep/extract.py +80 -8
  234. package/src/scripts/deep-sleep/synthesize-prompt.md +59 -2
  235. package/src/scripts/deep-sleep/synthesize.py +3 -1
  236. package/src/scripts/nexo-auto-update 2.py +6 -0
  237. package/src/scripts/nexo-backup 2.sh +25 -0
  238. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  239. package/src/scripts/nexo-catchup 2.py +242 -0
  240. package/src/scripts/nexo-catchup.py +65 -29
  241. package/src/scripts/nexo-cognitive-decay 2.py +182 -0
  242. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  243. package/src/scripts/nexo-daily-self-audit 2.py +552 -0
  244. package/src/scripts/nexo-daily-self-audit.py +4 -2
  245. package/src/scripts/nexo-deep-sleep 2.sh +97 -0
  246. package/src/scripts/nexo-deep-sleep.sh +66 -77
  247. package/src/scripts/nexo-evolution-run 2.py +597 -0
  248. package/src/scripts/nexo-evolution-run.py +13 -0
  249. package/src/scripts/nexo-followup-hygiene 2.py +112 -0
  250. package/src/scripts/nexo-immune 2.py +927 -0
  251. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  252. package/src/scripts/nexo-install 2.py +6 -0
  253. package/src/scripts/nexo-learning-housekeep 2.py +245 -0
  254. package/src/scripts/nexo-learning-housekeep.py +156 -1
  255. package/src/scripts/nexo-learning-validator 2.py +207 -0
  256. package/src/scripts/nexo-learning-validator.py +19 -0
  257. package/src/scripts/nexo-migrate 2.py +232 -0
  258. package/src/scripts/nexo-postmortem-consolidator 2.py +421 -0
  259. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  260. package/src/scripts/nexo-pre-commit 2.py +120 -0
  261. package/src/scripts/nexo-prevent-sleep 2.sh +29 -0
  262. package/src/scripts/nexo-proactive-dashboard 2.py +345 -0
  263. package/src/scripts/nexo-reflection 2.py +253 -0
  264. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  265. package/src/scripts/nexo-send-email 2.py +25 -0
  266. package/src/scripts/nexo-send-reply 2.py +178 -0
  267. package/src/scripts/nexo-sleep 2.py +592 -0
  268. package/src/scripts/nexo-sleep.py +16 -11
  269. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  270. package/src/scripts/nexo-synthesis 2.py +253 -0
  271. package/src/scripts/nexo-synthesis.py +46 -3
  272. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  273. package/src/scripts/nexo-update 2.sh +161 -0
  274. package/src/scripts/nexo-watchdog 2.sh +878 -0
  275. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  276. package/src/scripts/nexo-watchdog.sh +72 -19
  277. package/src/server 2.py +733 -0
  278. package/src/server.py +11 -2
  279. package/src/storage_router 2.py +32 -0
  280. package/src/tools_coordination 2.py +102 -0
  281. package/src/tools_credentials 2.py +68 -0
  282. package/src/tools_learnings 2.py +220 -0
  283. package/src/tools_menu 2.py +227 -0
  284. package/src/tools_reminders 2.py +86 -0
  285. package/src/tools_reminders_crud 2.py +159 -0
  286. package/src/tools_reminders_crud.py +7 -0
  287. package/src/tools_sessions 2.py +476 -0
  288. package/src/tools_task_history 2.py +57 -0
  289. package/templates/CLAUDE.md 2.template +63 -0
  290. package/templates/openclaw 2.json +13 -0
  291. package/tests/__init__ 2.py +0 -0
  292. package/tests/conftest 2.py +71 -0
  293. package/tests/test_cognitive 2.py +205 -0
  294. package/tests/test_knowledge_graph 2.py +140 -0
  295. package/tests/test_migrations 2.py +137 -0
  296. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  297. /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()