nexo-brain 2.0.0 → 2.2.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 (370) hide show
  1. package/README.md +143 -44
  2. package/bin/nexo-brain.js +53 -26
  3. package/package.json +15 -3
  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/pre-commit-check 2.sh +55 -0
  8. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  9. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  10. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  11. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  12. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  13. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  14. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  15. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  16. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  17. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  18. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  19. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  20. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  21. package/src/auto_close_sessions 2.py +159 -0
  22. package/src/auto_update 2.py +634 -0
  23. package/src/claim_graph 2.py +323 -0
  24. package/src/cognitive/__init__ 2.py +62 -0
  25. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  26. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  27. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  28. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  29. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  30. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  31. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  32. package/src/cognitive/_core 2.py +567 -0
  33. package/src/cognitive/_decay 2.py +382 -0
  34. package/src/cognitive/_ingest 2.py +892 -0
  35. package/src/cognitive/_memory 2.py +912 -0
  36. package/src/cognitive/_search 2.py +949 -0
  37. package/src/cognitive/_trust 2.py +464 -0
  38. package/src/cognitive/_trust.py +10 -36
  39. package/src/crons/manifest 2.json +106 -0
  40. package/src/crons/manifest.json +106 -0
  41. package/src/crons/sync 2.py +217 -0
  42. package/src/crons/sync.py +217 -0
  43. package/src/dashboard/__init__ 2.py +0 -0
  44. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  45. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  46. package/src/dashboard/app 2.py +789 -0
  47. package/src/dashboard/app.py +16 -2
  48. package/src/dashboard/templates/dashboard.html +3 -2
  49. package/src/db/__init__ 2.py +89 -0
  50. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  51. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  52. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  53. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  54. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  55. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  56. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  57. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  58. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  59. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  60. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  61. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  62. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  63. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  64. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  65. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  66. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  67. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  68. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  69. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  70. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  71. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  72. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  73. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  74. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  75. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  76. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  77. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  78. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  79. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  80. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  81. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  82. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  83. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  84. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  85. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  86. package/src/db/_core 2.py +417 -0
  87. package/src/db/_credentials 2.py +124 -0
  88. package/src/db/_entities 2.py +178 -0
  89. package/src/db/_episodic 2.py +738 -0
  90. package/src/db/_episodic.py +1 -1
  91. package/src/db/_evolution 2.py +54 -0
  92. package/src/db/_fts 2.py +406 -0
  93. package/src/db/_learnings 2.py +168 -0
  94. package/src/db/_reminders 2.py +338 -0
  95. package/src/db/_reminders.py +9 -5
  96. package/src/db/_schema 2.py +364 -0
  97. package/src/db/_sessions 2.py +300 -0
  98. package/src/db/_tasks 2.py +91 -0
  99. package/src/evolution_cycle 2.py +266 -0
  100. package/src/hnsw_index 2.py +254 -0
  101. package/src/hooks/auto_capture 2.py +208 -0
  102. package/src/hooks/caffeinate-guard 2.sh +8 -0
  103. package/src/hooks/capture-session 2.sh +21 -0
  104. package/src/hooks/capture-session.sh +2 -0
  105. package/src/hooks/capture-tool-logs 2.sh +127 -0
  106. package/src/hooks/capture-tool-logs.sh +3 -2
  107. package/src/hooks/daily-briefing-check 2.sh +33 -0
  108. package/src/hooks/inbox-hook 2.sh +76 -0
  109. package/src/hooks/inbox-hook.sh +3 -2
  110. package/src/hooks/post-compact 2.sh +148 -0
  111. package/src/hooks/post-compact.sh +1 -1
  112. package/src/hooks/pre-compact 2.sh +151 -0
  113. package/src/hooks/pre-compact.sh +1 -1
  114. package/src/hooks/session-start 2.sh +268 -0
  115. package/src/hooks/session-start.sh +6 -3
  116. package/src/hooks/session-stop 2.sh +140 -0
  117. package/src/hooks/session-stop.sh +3 -2
  118. package/src/kg_populate 2.py +290 -0
  119. package/src/knowledge_graph 2.py +257 -0
  120. package/src/maintenance 2.py +59 -0
  121. package/src/migrate_embeddings 2.py +122 -0
  122. package/src/plugin_loader 2.py +202 -0
  123. package/src/plugins/__init__ 2.py +0 -0
  124. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  125. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  126. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  127. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  128. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  129. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  130. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  131. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  132. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  133. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  134. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  135. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  136. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  137. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  138. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  139. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  140. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  141. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  142. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  143. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  144. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  145. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  146. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  147. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  148. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  149. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  150. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  151. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  152. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  154. package/src/plugins/adaptive_mode 2.py +805 -0
  155. package/src/plugins/agents 2.py +52 -0
  156. package/src/plugins/artifact_registry 2.py +450 -0
  157. package/src/plugins/backup 2.py +104 -0
  158. package/src/plugins/cognitive_memory 2.py +564 -0
  159. package/src/plugins/core_rules 2.py +252 -0
  160. package/src/plugins/core_rules.py +34 -17
  161. package/src/plugins/cortex 2.py +299 -0
  162. package/src/plugins/entities 2.py +67 -0
  163. package/src/plugins/episodic_memory 2.py +533 -0
  164. package/src/plugins/evolution 2.py +115 -0
  165. package/src/plugins/guard 2.py +746 -0
  166. package/src/plugins/knowledge_graph_tools 2.py +105 -0
  167. package/src/plugins/preferences 2.py +47 -0
  168. package/src/plugins/update 2.py +256 -0
  169. package/src/plugins/update.py +18 -0
  170. package/src/requirements 2.txt +12 -0
  171. package/src/rules/__init__ 2.py +0 -0
  172. package/src/rules/core-rules 2.json +331 -0
  173. package/src/rules/migrate 2.py +207 -0
  174. package/src/scripts/check-context 2.py +264 -0
  175. package/src/scripts/check-context.py +4 -7
  176. package/src/scripts/deep-sleep/apply_findings.py +570 -167
  177. package/src/scripts/deep-sleep/collect.py +480 -0
  178. package/src/scripts/deep-sleep/extract-prompt.md +233 -0
  179. package/src/scripts/deep-sleep/extract.py +249 -0
  180. package/src/scripts/deep-sleep/synthesize-prompt.md +197 -0
  181. package/src/scripts/deep-sleep/synthesize.py +191 -0
  182. package/src/scripts/nexo-auto-update 2.py +6 -0
  183. package/src/scripts/nexo-backup 2.sh +25 -0
  184. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  185. package/src/scripts/nexo-catchup 2.py +242 -0
  186. package/src/scripts/nexo-catchup.py +5 -8
  187. package/src/scripts/nexo-cognitive-decay 2.py +182 -0
  188. package/src/scripts/nexo-daily-self-audit 2.py +552 -0
  189. package/src/scripts/nexo-daily-self-audit.py +28 -19
  190. package/src/scripts/nexo-deep-sleep 2.sh +97 -0
  191. package/src/scripts/nexo-deep-sleep.sh +31 -16
  192. package/src/scripts/nexo-evolution-run 2.py +597 -0
  193. package/src/scripts/nexo-evolution-run.py +5 -20
  194. package/src/scripts/nexo-followup-hygiene 2.py +112 -0
  195. package/src/scripts/nexo-followup-hygiene.py +4 -2
  196. package/src/scripts/nexo-github-monitor 2.py +256 -0
  197. package/src/scripts/nexo-github-monitor.py +6 -9
  198. package/src/scripts/nexo-immune 2.py +927 -0
  199. package/src/scripts/nexo-immune.py +4 -17
  200. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  201. package/src/scripts/nexo-install 2.py +6 -0
  202. package/src/scripts/nexo-learning-housekeep 2.py +245 -0
  203. package/src/scripts/nexo-learning-validator 2.py +207 -0
  204. package/src/scripts/nexo-learning-validator.py +0 -29
  205. package/src/scripts/nexo-migrate 2.py +232 -0
  206. package/src/scripts/nexo-postmortem-consolidator 2.py +421 -0
  207. package/src/scripts/nexo-postmortem-consolidator.py +9 -20
  208. package/src/scripts/nexo-pre-commit 2.py +120 -0
  209. package/src/scripts/nexo-prevent-sleep 2.sh +29 -0
  210. package/src/scripts/nexo-proactive-dashboard 2.py +345 -0
  211. package/src/scripts/nexo-proactive-dashboard.py +1 -0
  212. package/src/scripts/nexo-reflection 2.py +253 -0
  213. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  214. package/src/scripts/nexo-send-email 2.py +25 -0
  215. package/src/scripts/nexo-send-reply 2.py +178 -0
  216. package/src/scripts/nexo-sleep 2.py +592 -0
  217. package/src/scripts/nexo-sleep.py +8 -18
  218. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  219. package/src/scripts/nexo-synthesis 2.py +253 -0
  220. package/src/scripts/nexo-synthesis.py +8 -19
  221. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  222. package/src/scripts/nexo-update 2.sh +161 -0
  223. package/src/scripts/nexo-watchdog 2.sh +878 -0
  224. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  225. package/src/server 2.py +733 -0
  226. package/src/server.py +6 -1
  227. package/src/storage_router 2.py +32 -0
  228. package/src/tools_coordination 2.py +102 -0
  229. package/src/tools_credentials 2.py +68 -0
  230. package/src/tools_learnings 2.py +220 -0
  231. package/src/tools_menu 2.py +227 -0
  232. package/src/tools_menu.py +1 -1
  233. package/src/tools_reminders 2.py +86 -0
  234. package/src/tools_reminders_crud 2.py +159 -0
  235. package/src/tools_reminders_crud.py +7 -0
  236. package/src/tools_sessions 2.py +476 -0
  237. package/src/tools_sessions.py +67 -0
  238. package/src/tools_task_history 2.py +57 -0
  239. package/templates/CLAUDE.md 2.template +63 -0
  240. package/templates/openclaw 2.json +13 -0
  241. package/tests/__init__ 2.py +0 -0
  242. package/tests/conftest 2.py +71 -0
  243. package/tests/test_cognitive 2.py +205 -0
  244. package/tests/test_knowledge_graph 2.py +140 -0
  245. package/tests/test_migrations 2.py +137 -0
  246. package/src/__pycache__/auto_close_sessions.cpython-310.pyc +0 -0
  247. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  248. package/src/__pycache__/auto_update.cpython-314.pyc +0 -0
  249. package/src/__pycache__/claim_graph.cpython-310.pyc +0 -0
  250. package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
  251. package/src/__pycache__/evolution_cycle.cpython-310.pyc +0 -0
  252. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  253. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  254. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  255. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  256. package/src/__pycache__/maintenance.cpython-310.pyc +0 -0
  257. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  258. package/src/__pycache__/migrate_embeddings.cpython-310.pyc +0 -0
  259. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  260. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  261. package/src/__pycache__/server.cpython-310.pyc +0 -0
  262. package/src/__pycache__/server.cpython-314.pyc +0 -0
  263. package/src/__pycache__/storage_router.cpython-310.pyc +0 -0
  264. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  265. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  266. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  267. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  268. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  269. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  270. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  271. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  272. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  273. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  274. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  275. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  276. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  277. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  278. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  279. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  280. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  281. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  282. package/src/hooks/__pycache__/auto_capture.cpython-310.pyc +0 -0
  283. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  284. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  285. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  286. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  287. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  288. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  289. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  290. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  291. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  292. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  293. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  294. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  295. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  296. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  297. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  298. package/src/rules/__pycache__/__init__.cpython-310.pyc +0 -0
  299. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  300. package/src/rules/__pycache__/migrate.cpython-310.pyc +0 -0
  301. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  302. package/src/scripts/__pycache__/check-context.cpython-310.pyc +0 -0
  303. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  304. package/src/scripts/__pycache__/nexo-auto-update.cpython-310.pyc +0 -0
  305. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  306. package/src/scripts/__pycache__/nexo-catchup.cpython-310.pyc +0 -0
  307. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  308. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-310.pyc +0 -0
  309. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  310. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-310.pyc +0 -0
  311. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  312. package/src/scripts/__pycache__/nexo-evolution-run.cpython-310.pyc +0 -0
  313. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  314. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-310.pyc +0 -0
  315. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  316. package/src/scripts/__pycache__/nexo-github-monitor.cpython-310.pyc +0 -0
  317. package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
  318. package/src/scripts/__pycache__/nexo-immune.cpython-310.pyc +0 -0
  319. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  320. package/src/scripts/__pycache__/nexo-install.cpython-310.pyc +0 -0
  321. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  322. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-310.pyc +0 -0
  323. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  324. package/src/scripts/__pycache__/nexo-learning-validator.cpython-310.pyc +0 -0
  325. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  326. package/src/scripts/__pycache__/nexo-migrate.cpython-310.pyc +0 -0
  327. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  328. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-310.pyc +0 -0
  329. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  330. package/src/scripts/__pycache__/nexo-pre-commit.cpython-310.pyc +0 -0
  331. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  332. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-310.pyc +0 -0
  333. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  334. package/src/scripts/__pycache__/nexo-reflection.cpython-310.pyc +0 -0
  335. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  336. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-310.pyc +0 -0
  337. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  338. package/src/scripts/__pycache__/nexo-send-email.cpython-310.pyc +0 -0
  339. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  340. package/src/scripts/__pycache__/nexo-send-reply.cpython-310.pyc +0 -0
  341. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  342. package/src/scripts/__pycache__/nexo-sleep.cpython-310.pyc +0 -0
  343. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  344. package/src/scripts/__pycache__/nexo-synthesis.cpython-310.pyc +0 -0
  345. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  346. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-310.pyc +0 -0
  347. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  348. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-310.pyc +0 -0
  349. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
  350. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-310.pyc +0 -0
  351. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
  352. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-310.pyc +0 -0
  353. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
  354. package/src/scripts/deep-sleep/analyze_session.py +0 -217
  355. package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
  356. package/src/scripts/deep-sleep/prompt.md +0 -109
  357. package/tests/__pycache__/__init__.cpython-310.pyc +0 -0
  358. package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  359. package/tests/__pycache__/conftest.cpython-310-pytest-9.0.2.pyc +0 -0
  360. package/tests/__pycache__/conftest.cpython-310.pyc +0 -0
  361. package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  362. package/tests/__pycache__/test_cognitive.cpython-310-pytest-9.0.2.pyc +0 -0
  363. package/tests/__pycache__/test_cognitive.cpython-310.pyc +0 -0
  364. package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
  365. package/tests/__pycache__/test_knowledge_graph.cpython-310-pytest-9.0.2.pyc +0 -0
  366. package/tests/__pycache__/test_knowledge_graph.cpython-310.pyc +0 -0
  367. package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
  368. package/tests/__pycache__/test_migrations.cpython-310-pytest-9.0.2.pyc +0 -0
  369. package/tests/__pycache__/test_migrations.cpython-310.pyc +0 -0
  370. package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -901,30 +901,17 @@ Raw findings:
901
901
  Write the report. Be concise — max 40 lines."""
902
902
 
903
903
  print("\n[TRIAGE] Running CLI interpretation...")
904
-
905
- # Verify Claude CLI is authenticated before calling
906
- try:
907
- auth_check = subprocess.run(
908
- [str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
909
- capture_output=True, text=True, timeout=15
910
- )
911
- if auth_check.returncode != 0:
912
- print("[TRIAGE] Claude CLI not available or not authenticated. Skipping triage.")
913
- return
914
- except Exception:
915
- print("[TRIAGE] Claude CLI check failed. Skipping triage.")
916
- return
917
-
918
904
  env = os.environ.copy()
905
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
919
906
  env.pop("CLAUDECODE", None)
920
907
  env.pop("CLAUDE_CODE", None)
921
908
 
922
909
  try:
923
910
  result = subprocess.run(
924
911
  [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
925
- "--output-format", "text", "--bare",
926
- "--allowedTools", "Read,Write,Edit,Glob,Grep"],
927
- capture_output=True, text=True, timeout=120, env=env
912
+ "--output-format", "text",
913
+ "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
914
+ capture_output=True, text=True, timeout=21600, env=env
928
915
  )
929
916
  if result.returncode == 0:
930
917
  print(f"[TRIAGE] Report written to {triage_file}")
@@ -0,0 +1,74 @@
1
+ #!/bin/bash
2
+ # nexo-inbox-hook.sh — PostToolUse: automatic inter-terminal inbox check (D+)
3
+ #
4
+ # Zero output when no messages = zero tokens consumed in Claude's context.
5
+ # Reads SQLite directly (no MCP overhead). Write-only: INSERT OR IGNORE for mark-as-read.
6
+ # Debounce: skips if last check was <2 seconds ago.
7
+
8
+ INPUT=$(cat)
9
+
10
+ # 1. Skip read-only tools (same logic as capture-tool-logs.sh)
11
+ TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
12
+ case "$TOOL_NAME" in
13
+ Read|Grep|Glob|LS|Skill|ToolSearch|Agent) exit 0 ;;
14
+ esac
15
+
16
+ # 2. Extract Claude Code session_id
17
+ CLAUDE_SID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" 2>/dev/null)
18
+ [ -z "$CLAUDE_SID" ] && exit 0
19
+
20
+ # 3. Debounce: skip if last check <2s ago
21
+ DEBOUNCE_FILE="/tmp/nexo-inbox-ts-${CLAUDE_SID}"
22
+ NOW=$(date +%s)
23
+ LAST=$(cat "$DEBOUNCE_FILE" 2>/dev/null || echo 0)
24
+ DIFF=$((NOW - LAST))
25
+ [ "$DIFF" -lt 2 ] && exit 0
26
+ echo "$NOW" > "$DEBOUNCE_FILE"
27
+
28
+ # 4. Find NEXO SID mapped to this Claude session_id
29
+ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
30
+ DB="$NEXO_HOME/data/nexo.db"
31
+ [ -f "$DB" ] || exit 0
32
+
33
+ NEXO_SID=$(sqlite3 "$DB" "SELECT sid FROM sessions WHERE claude_session_id = '${CLAUDE_SID}' AND last_update_epoch > (strftime('%s','now') - 900) ORDER BY last_update_epoch DESC LIMIT 1;" 2>/dev/null)
34
+ [ -z "$NEXO_SID" ] && exit 0
35
+
36
+ # 5. Check inbox — messages addressed to this session or broadcast
37
+ MESSAGES=$(sqlite3 -separator '|' "$DB" "
38
+ SELECT m.id, m.from_sid, m.text FROM messages m
39
+ WHERE (m.to_sid = 'all' OR m.to_sid = '${NEXO_SID}')
40
+ AND m.from_sid != '${NEXO_SID}'
41
+ AND m.id NOT IN (SELECT message_id FROM message_reads WHERE sid = '${NEXO_SID}')
42
+ LIMIT 5;
43
+ " 2>/dev/null)
44
+
45
+ # 6. Check pending questions
46
+ QUESTIONS=$(sqlite3 -separator '|' "$DB" "
47
+ SELECT qid, from_sid, question FROM questions
48
+ WHERE to_sid = '${NEXO_SID}' AND answer IS NULL
49
+ LIMIT 3;
50
+ " 2>/dev/null)
51
+
52
+ # 7. If empty → silent exit (0 tokens consumed)
53
+ [ -z "$MESSAGES" ] && [ -z "$QUESTIONS" ] && exit 0
54
+
55
+ # 8. Format and output (injected into Claude's context)
56
+ echo ""
57
+ echo "📨 INTER-TERMINAL MESSAGE (auto-detected):"
58
+
59
+ if [ -n "$MESSAGES" ]; then
60
+ echo "$MESSAGES" | while IFS='|' read -r mid from text; do
61
+ echo " [$from]: $text"
62
+ # Mark as read (lightweight INSERT, WAL mode, no lock contention)
63
+ sqlite3 "$DB" "INSERT OR IGNORE INTO message_reads (message_id, sid) VALUES ('${mid}', '${NEXO_SID}');" 2>/dev/null
64
+ done
65
+ fi
66
+
67
+ if [ -n "$QUESTIONS" ]; then
68
+ echo " ⚠ PREGUNTAS de otra terminal — responder con nexo_answer:"
69
+ echo "$QUESTIONS" | while IFS='|' read -r qid from question; do
70
+ echo " Q[$qid] de [$from]: $question"
71
+ done
72
+ fi
73
+
74
+ exit 0
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+ """DEPRECATED: Use 'npx nexo-brain' instead. This installer is no longer maintained."""
3
+ import sys
4
+ print("This installer is deprecated. Please use: npx nexo-brain")
5
+ print("See: https://github.com/wazionapps/nexo#installation")
6
+ sys.exit(1)
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env python3
2
+ """NEXO Learning Housekeeping — Nightly dedup, weight adjustment, and review.
3
+
4
+ Runs daily. Adjusts learning weights based on usage (guard_hits),
5
+ detects duplicates via semantic similarity, and archives stale learnings.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sqlite3
11
+ import sys
12
+ import time
13
+ from datetime import datetime, timedelta
14
+ from pathlib import Path
15
+
16
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
17
+ # Auto-detect: if running from repo (src/scripts/), use src/ as NEXO_CODE
18
+ _script_dir = Path(__file__).resolve().parent
19
+ _repo_src = _script_dir.parent # src/scripts/ -> src/
20
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
21
+
22
+ sys.path.insert(0, str(NEXO_CODE))
23
+
24
+ DB_PATH = NEXO_HOME / "data" / "nexo.db"
25
+ STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
26
+
27
+ # Weight adjustment rates
28
+ GUARD_HIT_BOOST = 0.02 # per guard hit since last run
29
+ DECAY_RATE = 0.005 # daily decay for unused learnings
30
+ MIN_WEIGHT = 0.05
31
+ MAX_WEIGHT = 1.0
32
+ DEDUP_THRESHOLD = 0.85 # cosine similarity for duplicate detection
33
+ ARCHIVE_AFTER_DAYS = 90 # archive if weight < 0.1 and no hits in this many days
34
+
35
+
36
+ def get_db():
37
+ conn = sqlite3.connect(str(DB_PATH))
38
+ conn.row_factory = sqlite3.Row
39
+ return conn
40
+
41
+
42
+ def update_catchup_state():
43
+ try:
44
+ state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
45
+ except Exception:
46
+ state = {}
47
+ state["learning-housekeep"] = datetime.now().isoformat()
48
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
49
+ STATE_FILE.write_text(json.dumps(state, indent=2))
50
+
51
+
52
+ def adjust_weights(conn):
53
+ """Boost weight for frequently-used learnings, decay unused ones."""
54
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
55
+ now = time.time()
56
+ one_day_ago = now - 86400
57
+
58
+ learnings = conn.execute(
59
+ "SELECT id, weight, guard_hits, last_guard_hit_at, priority, created_at "
60
+ "FROM learnings WHERE status = 'active'"
61
+ ).fetchall()
62
+
63
+ adjusted = 0
64
+ for l in learnings:
65
+ old_weight = l["weight"] or 0.5
66
+ hits = l["guard_hits"] or 0
67
+ last_hit = l["last_guard_hit_at"] or 0
68
+ priority = l["priority"] or "medium"
69
+
70
+ # Priority floor — critical learnings never drop below 0.5
71
+ priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}[priority]
72
+
73
+ new_weight = old_weight
74
+
75
+ if last_hit > one_day_ago:
76
+ # Recent guard hit — boost
77
+ recent_hits = 1 # Simplified: at least 1 hit today
78
+ new_weight = min(MAX_WEIGHT, old_weight + (GUARD_HIT_BOOST * recent_hits))
79
+ else:
80
+ # No recent hits — decay
81
+ new_weight = max(priority_floor, old_weight - DECAY_RATE)
82
+
83
+ new_weight = max(MIN_WEIGHT, min(MAX_WEIGHT, new_weight))
84
+
85
+ if abs(new_weight - old_weight) > 0.001:
86
+ conn.execute("UPDATE learnings SET weight = ? WHERE id = ?", (round(new_weight, 4), l["id"]))
87
+ adjusted += 1
88
+
89
+ conn.commit()
90
+ print(f"[{ts}] Weight adjustment: {adjusted}/{len(learnings)} learnings adjusted")
91
+ return adjusted
92
+
93
+
94
+ def auto_prioritize(conn):
95
+ """Auto-upgrade priority based on guard hits and repetitions."""
96
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
97
+
98
+ # Learnings with 10+ guard hits that are still medium → upgrade to high
99
+ upgraded = conn.execute(
100
+ "UPDATE learnings SET priority = 'high', weight = MAX(weight, 0.7) "
101
+ "WHERE status = 'active' AND priority = 'medium' AND guard_hits >= 10"
102
+ ).rowcount
103
+
104
+ # Learnings with repetitions (same error happened again) → upgrade to high
105
+ repeated = conn.execute(
106
+ """UPDATE learnings SET priority = 'high', weight = MAX(weight, 0.7)
107
+ WHERE status = 'active' AND priority IN ('medium', 'low')
108
+ AND id IN (SELECT original_learning_id FROM error_repetitions)"""
109
+ ).rowcount
110
+
111
+ conn.commit()
112
+ total = upgraded + repeated
113
+ if total > 0:
114
+ print(f"[{ts}] Auto-prioritize: {upgraded} by guard_hits, {repeated} by repetitions")
115
+ return total
116
+
117
+
118
+ def detect_duplicates(conn):
119
+ """Find semantically similar learnings using fastembed."""
120
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
121
+ try:
122
+ from fastembed import TextEmbedding
123
+ import numpy as np
124
+ except ImportError:
125
+ print(f"[{ts}] Dedup skipped: fastembed not available")
126
+ return []
127
+
128
+ learnings = conn.execute(
129
+ "SELECT id, title, content, weight, guard_hits FROM learnings WHERE status = 'active'"
130
+ ).fetchall()
131
+
132
+ if len(learnings) < 2:
133
+ return []
134
+
135
+ model = TextEmbedding("BAAI/bge-base-en-v1.5")
136
+ texts = [f"{l['title']}: {l['content'][:300]}" for l in learnings]
137
+ embeddings = list(model.embed(texts))
138
+ embeddings = np.array(embeddings)
139
+
140
+ # Normalize
141
+ norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
142
+ norms[norms == 0] = 1
143
+ embeddings = embeddings / norms
144
+
145
+ duplicates = []
146
+ for i in range(len(learnings)):
147
+ for j in range(i + 1, len(learnings)):
148
+ sim = float(np.dot(embeddings[i], embeddings[j]))
149
+ if sim >= DEDUP_THRESHOLD:
150
+ # Keep the one with higher weight/hits
151
+ a, b = learnings[i], learnings[j]
152
+ score_a = (a["weight"] or 0.5) + (a["guard_hits"] or 0) * 0.01
153
+ score_b = (b["weight"] or 0.5) + (b["guard_hits"] or 0) * 0.01
154
+ keep, drop = (a, b) if score_a >= score_b else (b, a)
155
+ duplicates.append({
156
+ "keep_id": keep["id"], "keep_title": keep["title"],
157
+ "drop_id": drop["id"], "drop_title": drop["title"],
158
+ "similarity": round(sim, 3)
159
+ })
160
+
161
+ if duplicates:
162
+ print(f"[{ts}] Duplicates found: {len(duplicates)} pairs (>= {DEDUP_THRESHOLD})")
163
+ for d in duplicates[:10]:
164
+ print(f"[{ts}] [{d['similarity']}] keep #{d['keep_id']} '{d['keep_title'][:40]}', archive #{d['drop_id']} '{d['drop_title'][:40]}'")
165
+ # Archive the duplicate (don't delete — just mark inactive)
166
+ conn.execute("UPDATE learnings SET status = 'archived' WHERE id = ?", (d["drop_id"],))
167
+ conn.commit()
168
+ else:
169
+ print(f"[{ts}] No duplicates found ({len(learnings)} learnings scanned)")
170
+
171
+ return duplicates
172
+
173
+
174
+ def archive_stale(conn):
175
+ """Archive learnings with very low weight and no recent guard hits."""
176
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
177
+ cutoff = time.time() - (ARCHIVE_AFTER_DAYS * 86400)
178
+
179
+ stale = conn.execute(
180
+ "SELECT id, title, weight, last_guard_hit_at FROM learnings "
181
+ "WHERE status = 'active' AND weight < 0.1 AND priority NOT IN ('critical', 'high') "
182
+ "AND (last_guard_hit_at IS NULL OR last_guard_hit_at < ?)",
183
+ (cutoff,)
184
+ ).fetchall()
185
+
186
+ if stale:
187
+ for s in stale:
188
+ conn.execute("UPDATE learnings SET status = 'archived' WHERE id = ?", (s["id"],))
189
+ print(f"[{ts}] Archived #{s['id']} '{s['title'][:50]}' (weight={s['weight']:.2f})")
190
+ conn.commit()
191
+ print(f"[{ts}] Archived {len(stale)} stale learnings")
192
+ else:
193
+ print(f"[{ts}] No stale learnings to archive")
194
+
195
+ return len(stale)
196
+
197
+
198
+ def print_summary(conn):
199
+ """Print summary stats."""
200
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
201
+ stats = conn.execute(
202
+ """SELECT
203
+ COUNT(*) as total,
204
+ SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
205
+ SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) as archived,
206
+ SUM(CASE WHEN priority = 'critical' THEN 1 ELSE 0 END) as critical,
207
+ SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) as high,
208
+ SUM(CASE WHEN priority = 'medium' THEN 1 ELSE 0 END) as medium,
209
+ SUM(CASE WHEN priority = 'low' THEN 1 ELSE 0 END) as low,
210
+ printf('%.2f', AVG(CASE WHEN status = 'active' THEN weight END)) as avg_weight
211
+ FROM learnings"""
212
+ ).fetchone()
213
+ print(f"[{ts}] Summary: {stats['active']} active, {stats['archived']} archived | "
214
+ f"Priority: {stats['critical']}C {stats['high']}H {stats['medium']}M {stats['low']}L | "
215
+ f"Avg weight: {stats['avg_weight']}")
216
+
217
+
218
+ def main():
219
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
220
+ print(f"[{ts}] Learning housekeeping starting...")
221
+
222
+ conn = get_db()
223
+
224
+ # 1. Adjust weights based on usage
225
+ adjust_weights(conn)
226
+
227
+ # 2. Auto-prioritize based on guard hits and repetitions
228
+ auto_prioritize(conn)
229
+
230
+ # 3. Detect and archive duplicates
231
+ detect_duplicates(conn)
232
+
233
+ # 4. Archive stale learnings
234
+ archive_stale(conn)
235
+
236
+ # 5. Summary
237
+ print_summary(conn)
238
+
239
+ conn.close()
240
+ update_catchup_state()
241
+ print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Done.")
242
+
243
+
244
+ if __name__ == "__main__":
245
+ main()
@@ -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,35 +111,6 @@ Rules:
111
111
 
112
112
  # Try CLI first, fall back to mechanical similarity
113
113
  if CLAUDE_CLI.exists():
114
- auth_check = subprocess.run(
115
- [str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
116
- capture_output=True, text=True, timeout=15
117
- )
118
- if auth_check.returncode != 0:
119
- # CLI not authenticated, skip gracefully
120
- return {"known": False, "confidence": 0, "recommendation": "CLI not authenticated — skipped validation", "matching_learnings": []}
121
-
122
- env = os.environ.copy()
123
- env.pop("CLAUDECODE", None)
124
- env.pop("CLAUDE_CODE", None)
125
-
126
- try:
127
- result = subprocess.run(
128
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text", "--bare",
129
- "--allowedTools", "Read,Write,Edit,Glob,Grep"],
130
- capture_output=True, text=True, timeout=60, env=env
131
- )
132
- if result.returncode == 0:
133
- text = result.stdout.strip()
134
- # Strip markdown fences if present
135
- if "```json" in text:
136
- text = text.split("```json")[1].split("```")[0]
137
- elif "```" in text:
138
- text = text.split("```")[1].split("```")[0]
139
- return json.loads(text.strip())
140
- except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception):
141
- pass # Fall through to mechanical fallback
142
-
143
114
  # Fallback: mechanical SequenceMatcher (original logic)
144
115
  return _mechanical_validate(finding, learnings)
145
116