nexo-brain 2.3.0 → 2.3.1

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 (287) hide show
  1. package/README.md +1 -1
  2. package/package.json +6 -3
  3. package/src/auto_update.py +1 -0
  4. package/src/crons/sync.py +1 -2
  5. package/src/db/_core.py +1 -0
  6. package/src/db/_entities.py +1 -0
  7. package/src/db/_episodic.py +1 -0
  8. package/src/db/_learnings.py +1 -0
  9. package/src/db/_reminders.py +1 -0
  10. package/src/db/_sessions.py +1 -0
  11. package/src/db/_skills.py +1 -0
  12. package/src/plugin_loader.py +1 -0
  13. package/src/plugins/update.py +1 -0
  14. package/src/scripts/deep-sleep/apply_findings.py +1 -0
  15. package/src/scripts/deep-sleep/collect.py +1 -0
  16. package/src/scripts/deep-sleep/extract.py +1 -0
  17. package/src/scripts/deep-sleep/synthesize.py +1 -0
  18. package/src/scripts/nexo-learning-housekeep.py +1 -0
  19. package/src/scripts/nexo-watchdog.sh +19 -11
  20. package/src/server.py +1 -0
  21. package/src/tools_coordination.py +1 -0
  22. package/src/tools_sessions.py +1 -0
  23. package/scripts/migrate-to-unified 2.sh +0 -813
  24. package/scripts/migrate-to-unified.sh +0 -813
  25. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  26. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  27. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  28. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  29. package/scripts/nexo-preflight.sh +0 -236
  30. package/scripts/pre-commit-check 2.sh +0 -55
  31. package/scripts/pre-commit-check.sh +0 -55
  32. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  33. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  34. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  35. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  36. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  37. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  38. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  39. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  40. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  41. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  42. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  43. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  44. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  45. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  46. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  47. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  48. package/src/auto_close_sessions 2.py +0 -159
  49. package/src/auto_update 2.py +0 -634
  50. package/src/claim_graph 2.py +0 -323
  51. package/src/cognitive/__init__ 2.py +0 -62
  52. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  53. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  54. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  55. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  56. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  57. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  58. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  59. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  60. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  61. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  62. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  63. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  64. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  65. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  66. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  67. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  69. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  70. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  72. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  73. package/src/cognitive/_core 2.py +0 -567
  74. package/src/cognitive/_decay 2.py +0 -382
  75. package/src/cognitive/_ingest 2.py +0 -892
  76. package/src/cognitive/_memory 2.py +0 -912
  77. package/src/cognitive/_search 2.py +0 -949
  78. package/src/cognitive/_trust 2.py +0 -464
  79. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  80. package/src/crons/manifest 2.json +0 -106
  81. package/src/crons/sync 2.py +0 -217
  82. package/src/dashboard/__init__ 2.py +0 -0
  83. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  84. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  85. package/src/dashboard/app 2.py +0 -789
  86. package/src/db/__init__ 2.py +0 -89
  87. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  98. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  99. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  100. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  101. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  102. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  103. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  104. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  105. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  106. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  107. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  108. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  109. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  110. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  111. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  112. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  113. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  114. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  115. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  116. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  117. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  118. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  119. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  120. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  121. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  122. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  123. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  124. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  125. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  126. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  127. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  128. package/src/db/_core 2.py +0 -417
  129. package/src/db/_credentials 2.py +0 -124
  130. package/src/db/_entities 2.py +0 -178
  131. package/src/db/_episodic 2.py +0 -738
  132. package/src/db/_evolution 2.py +0 -54
  133. package/src/db/_fts 2.py +0 -406
  134. package/src/db/_learnings 2.py +0 -168
  135. package/src/db/_reminders 2.py +0 -338
  136. package/src/db/_schema 2.py +0 -364
  137. package/src/db/_sessions 2.py +0 -300
  138. package/src/db/_tasks 2.py +0 -91
  139. package/src/evolution_cycle 2.py +0 -266
  140. package/src/hnsw_index 2.py +0 -254
  141. package/src/hooks/auto_capture 2.py +0 -208
  142. package/src/hooks/caffeinate-guard 2.sh +0 -8
  143. package/src/hooks/capture-session 2.sh +0 -21
  144. package/src/hooks/capture-tool-logs 2.sh +0 -127
  145. package/src/hooks/daily-briefing-check 2.sh +0 -33
  146. package/src/hooks/inbox-hook 2.sh +0 -76
  147. package/src/hooks/post-compact 2.sh +0 -148
  148. package/src/hooks/pre-compact 2.sh +0 -151
  149. package/src/hooks/session-start 2.sh +0 -268
  150. package/src/hooks/session-stop 2.sh +0 -140
  151. package/src/kg_populate 2.py +0 -290
  152. package/src/knowledge_graph 2.py +0 -257
  153. package/src/maintenance 2.py +0 -59
  154. package/src/migrate_embeddings 2.py +0 -122
  155. package/src/plugin_loader 2.py +0 -202
  156. package/src/plugins/__init__ 2.py +0 -0
  157. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  160. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  163. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  183. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  185. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  187. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  188. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  189. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  190. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  191. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  192. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  193. package/src/plugins/adaptive_mode 2.py +0 -805
  194. package/src/plugins/agents 2.py +0 -52
  195. package/src/plugins/artifact_registry 2.py +0 -450
  196. package/src/plugins/backup 2.py +0 -104
  197. package/src/plugins/cognitive_memory 2.py +0 -564
  198. package/src/plugins/core_rules 2.py +0 -252
  199. package/src/plugins/cortex 2.py +0 -299
  200. package/src/plugins/entities 2.py +0 -67
  201. package/src/plugins/episodic_memory 2.py +0 -533
  202. package/src/plugins/evolution 2.py +0 -115
  203. package/src/plugins/guard 2.py +0 -746
  204. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  205. package/src/plugins/preferences 2.py +0 -47
  206. package/src/plugins/update 2.py +0 -256
  207. package/src/requirements 2.txt +0 -12
  208. package/src/rules/__init__ 2.py +0 -0
  209. package/src/rules/core-rules 2.json +0 -331
  210. package/src/rules/migrate 2.py +0 -207
  211. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  216. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  217. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  218. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  219. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  220. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  221. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  222. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  223. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  232. package/src/scripts/check-context 2.py +0 -264
  233. package/src/scripts/nexo-auto-update 2.py +0 -6
  234. package/src/scripts/nexo-backup 2.sh +0 -25
  235. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  236. package/src/scripts/nexo-catchup 2.py +0 -242
  237. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  238. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  239. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  240. package/src/scripts/nexo-evolution-run 2.py +0 -597
  241. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  242. package/src/scripts/nexo-github-monitor 2.py +0 -256
  243. package/src/scripts/nexo-immune 2.py +0 -927
  244. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  245. package/src/scripts/nexo-install 2.py +0 -6
  246. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  247. package/src/scripts/nexo-learning-validator 2.py +0 -207
  248. package/src/scripts/nexo-migrate 2.py +0 -232
  249. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  250. package/src/scripts/nexo-pre-commit 2.py +0 -120
  251. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  252. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  253. package/src/scripts/nexo-reflection 2.py +0 -253
  254. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  255. package/src/scripts/nexo-send-email 2.py +0 -25
  256. package/src/scripts/nexo-send-email.py +0 -25
  257. package/src/scripts/nexo-send-reply 2.py +0 -178
  258. package/src/scripts/nexo-send-reply.py +0 -178
  259. package/src/scripts/nexo-sleep 2.py +0 -592
  260. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  261. package/src/scripts/nexo-synthesis 2.py +0 -253
  262. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  263. package/src/scripts/nexo-update 2.sh +0 -161
  264. package/src/scripts/nexo-watchdog 2.sh +0 -878
  265. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  266. package/src/server 2.py +0 -733
  267. package/src/storage_router 2.py +0 -32
  268. package/src/tools_coordination 2.py +0 -102
  269. package/src/tools_credentials 2.py +0 -68
  270. package/src/tools_learnings 2.py +0 -220
  271. package/src/tools_menu 2.py +0 -227
  272. package/src/tools_reminders 2.py +0 -86
  273. package/src/tools_reminders_crud 2.py +0 -159
  274. package/src/tools_sessions 2.py +0 -476
  275. package/src/tools_task_history 2.py +0 -57
  276. package/templates/CLAUDE.md 2.template +0 -63
  277. package/templates/openclaw 2.json +0 -13
  278. package/tests/__init__ 2.py +0 -0
  279. package/tests/__init__.py +0 -0
  280. package/tests/conftest 2.py +0 -71
  281. package/tests/conftest.py +0 -71
  282. package/tests/test_cognitive 2.py +0 -205
  283. package/tests/test_cognitive.py +0 -205
  284. package/tests/test_knowledge_graph 2.py +0 -140
  285. package/tests/test_knowledge_graph.py +0 -140
  286. package/tests/test_migrations 2.py +0 -137
  287. package/tests/test_migrations.py +0 -137
@@ -1,746 +0,0 @@
1
- """Guard plugin — Error prevention closed-loop system.
2
-
3
- Surfaces relevant learnings at the moment of action, tracks repetitions,
4
- and provides stats on error prevention effectiveness.
5
- """
6
- import json
7
- import os
8
- from datetime import datetime, timedelta
9
- from db import get_db, find_similar_learnings, extract_keywords, search_learnings, search_changes
10
-
11
-
12
-
13
- def _load_schema_cache() -> dict:
14
- """Load cached DB schemas from schema_cache.json."""
15
- try:
16
- path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "schema_cache.json")
17
- if os.path.exists(path):
18
- with open(path) as f:
19
- return json.load(f)
20
- except Exception:
21
- pass
22
- return {}
23
-
24
-
25
- def _get_nexo_table_schema(table_name: str) -> str:
26
- """Get schema for a nexo.db table via PRAGMA."""
27
- conn = get_db()
28
- try:
29
- rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall()
30
- if rows:
31
- cols = [f"{r['name']}({r['type']})" for r in rows]
32
- return ", ".join(cols)
33
- except Exception:
34
- pass
35
- return ""
36
-
37
-
38
- def _extract_table_names(content: str) -> set:
39
- """Extract SQL table names from source code."""
40
- import re
41
- tables = set()
42
- # Match FROM/JOIN/INTO/UPDATE/TABLE patterns
43
- patterns = [
44
- r'(?:FROM|JOIN|INTO|UPDATE)\s+`?(\w+)`?',
45
- r'CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?',
46
- r'DESCRIBE\s+`?(\w+)`?',
47
- r'table_info\([\'\"]?(\w+)[\'\"]?\)',
48
- ]
49
- for pat in patterns:
50
- for m in re.finditer(pat, content, re.IGNORECASE):
51
- tables.add(m.group(1))
52
- # Filter out SQL keywords that might match
53
- sql_keywords = {'SELECT', 'WHERE', 'AND', 'OR', 'NOT', 'NULL', 'SET', 'VALUES', 'INTO', 'AS'}
54
- return {t for t in tables if t.upper() not in sql_keywords}
55
-
56
-
57
- def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "true") -> str:
58
- """Check learnings relevant to files/area before editing. Call BEFORE any code change.
59
-
60
- Args:
61
- files: Comma-separated file paths about to be edited
62
- area: System area (webapp, shopify, infrastructure, nexo-ops, etc.)
63
- include_schemas: Include DB table schemas if files touch database code (true/false)
64
- """
65
- conn = get_db()
66
- include_schemas_bool = include_schemas.lower() in ("true", "1", "yes")
67
- file_list = [f.strip() for f in files.split(",") if f.strip()] if files else []
68
-
69
- result = {
70
- "learnings": [],
71
- "universal_rules": [],
72
- "schemas": {},
73
- "area_repetition_rate": 0.0,
74
- "blocking_rules": [],
75
- }
76
-
77
- seen_ids = set()
78
-
79
- # 1. By file path — learnings mentioning the file name or parent directory
80
- hit_ids = []
81
- for filepath in file_list:
82
- from pathlib import Path
83
- p = Path(filepath)
84
- filename = p.name
85
- parent_dir = p.parent.name
86
-
87
- rows = conn.execute(
88
- "SELECT id, category, title, content, priority, weight FROM learnings WHERE INSTR(content, ?) > 0 OR INSTR(content, ?) > 0",
89
- (filename, parent_dir)
90
- ).fetchall()
91
- for r in rows:
92
- if r["id"] not in seen_ids:
93
- seen_ids.add(r["id"])
94
- hit_ids.append(r["id"])
95
- pri = r["priority"] or "medium"
96
- w = r["weight"] or 0.5
97
- result["learnings"].append({"id": r["id"], "category": r["category"], "rule": r["title"], "priority": pri, "weight": w})
98
-
99
- # 2. By area/category
100
- if area:
101
- rows = conn.execute(
102
- "SELECT id, category, title, content, priority, weight FROM learnings WHERE category = ?",
103
- (area,)
104
- ).fetchall()
105
- for r in rows:
106
- if r["id"] not in seen_ids:
107
- seen_ids.add(r["id"])
108
- hit_ids.append(r["id"])
109
- pri = r["priority"] or "medium"
110
- w = r["weight"] or 0.5
111
- result["learnings"].append({"id": r["id"], "category": r["category"], "rule": r["title"], "priority": pri, "weight": w})
112
-
113
- # 3. Universal rules — only from matching area or nexo-ops (not ALL learnings)
114
- universal_categories = {"nexo-ops"}
115
- if area:
116
- universal_categories.add(area)
117
- placeholders = ",".join("?" for _ in universal_categories)
118
- rows = conn.execute(
119
- f"SELECT id, category, title, content, priority FROM learnings WHERE "
120
- f"category IN ({placeholders}) AND ("
121
- f"content LIKE '%SIEMPRE%' OR content LIKE '%NUNCA%' OR content LIKE '%ANTES%' "
122
- f"OR content LIKE '%always%' OR content LIKE '%never%')",
123
- tuple(universal_categories)
124
- ).fetchall()
125
- for r in rows:
126
- if r["id"] not in seen_ids:
127
- seen_ids.add(r["id"])
128
- result["universal_rules"].append({"id": r["id"], "rule": r["title"], "category": r["category"], "priority": r["priority"] or "medium"})
129
-
130
- # 4. DB schemas if files contain SQL keywords
131
- if include_schemas_bool and file_list:
132
- all_tables = set()
133
- for filepath in file_list:
134
- try:
135
- with open(filepath, 'r', errors='ignore') as f:
136
- content = f.read()
137
- sql_keywords = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE TABLE']
138
- if any(kw in content.upper() for kw in sql_keywords):
139
- all_tables.update(_extract_table_names(content))
140
- except (FileNotFoundError, PermissionError):
141
- continue
142
-
143
- cache = _load_schema_cache()
144
- for table in all_tables:
145
- # Try nexo.db first
146
- schema = _get_nexo_table_schema(table)
147
- if schema:
148
- result["schemas"][table] = schema
149
- elif "cloud_sql" in cache and table in cache["cloud_sql"]:
150
- result["schemas"][table] = cache["cloud_sql"][table]
151
-
152
- # 5. Check for blocking rules — two paths:
153
- # (a) 5+ repetitions (existing behavior)
154
- # (b) Learning contains NUNCA/NEVER/PROHIBIDO and matches semantically (aggressive mode)
155
- import re
156
- BLOCKING_KEYWORDS = re.compile(
157
- r'\bNUNCA\b|\bNEVER\b|\bPROHIBIDO\b|\bNO\s+\w+\b|\bFORBIDDEN\b|\bBLOCKING\b|\bSIEMPRE\b|\bALWAYS\b',
158
- re.IGNORECASE
159
- )
160
- # Check both learnings and universal_rules for blocking
161
- all_candidates = [(l, "learning") for l in result["learnings"]] + \
162
- [(u, "universal") for u in result["universal_rules"]]
163
- blocking_seen = set()
164
- for learning, source in all_candidates:
165
- lid = learning["id"]
166
- if lid in blocking_seen:
167
- continue
168
- rep_count = conn.execute(
169
- "SELECT COUNT(*) as cnt FROM error_repetitions WHERE original_learning_id = ?",
170
- (lid,)
171
- ).fetchone()["cnt"]
172
-
173
- # Path (a): 5+ repetitions
174
- if rep_count >= 5:
175
- blocking_seen.add(lid)
176
- result["blocking_rules"].append({
177
- "id": lid, "rule": learning["rule"], "repetitions": rep_count,
178
- "reason": "repeated_error"
179
- })
180
- continue
181
-
182
- # Path (b): Only promote to blocking if high/critical priority AND title has prohibition keyword
183
- pri = learning.get("priority", "medium")
184
- if pri in ("critical", "high") and BLOCKING_KEYWORDS.search(learning["rule"]):
185
- blocking_seen.add(lid)
186
- result["blocking_rules"].append({
187
- "id": lid, "rule": learning["rule"], "repetitions": rep_count,
188
- "reason": "prohibition_keyword"
189
- })
190
-
191
- # 5b. Behavioral rules — when called without files (session-level check)
192
- if not file_list:
193
- behavioral = conn.execute(
194
- """SELECT l.id, l.title, l.category, COUNT(e.id) as violations
195
- FROM learnings l
196
- LEFT JOIN error_repetitions e ON e.original_learning_id = l.id
197
- WHERE l.category = 'nexo-ops' AND l.status = 'active'
198
- GROUP BY l.id
199
- ORDER BY violations DESC, l.created_at DESC
200
- LIMIT 5"""
201
- ).fetchall()
202
- if behavioral:
203
- result["behavioral_rules"] = [
204
- {"id": r["id"], "rule": r["title"], "violations": r["violations"]}
205
- for r in behavioral
206
- ]
207
-
208
- # 6. Area repetition rate
209
- if area:
210
- total_area = conn.execute(
211
- "SELECT COUNT(*) as cnt FROM learnings WHERE category = ?", (area,)
212
- ).fetchone()["cnt"]
213
- reps_area = conn.execute(
214
- "SELECT COUNT(*) as cnt FROM error_repetitions WHERE area = ?", (area,)
215
- ).fetchone()["cnt"]
216
- if total_area > 0:
217
- result["area_repetition_rate"] = round(reps_area / total_area, 2)
218
-
219
- # 7. Cognitive metacognition — semantic search for related warnings
220
- # Trust score modulates rigor: <40 = paranoid mode (more results, lower threshold)
221
- cognitive_warnings = []
222
- trust_note = ""
223
- try:
224
- import cognitive
225
- trust = cognitive.get_trust_score()
226
-
227
- # Rigor modulation based on trust
228
- if trust < 40:
229
- cog_top_k = 6 # More results
230
- cog_min_score = 0.55 # Lower threshold = catch more
231
- trust_note = f" [RIGOR: PARANOID — trust={trust:.0f}]"
232
- elif trust > 80:
233
- cog_top_k = 2 # Fewer results
234
- cog_min_score = 0.75 # Higher threshold = only strong matches
235
- trust_note = f" [RIGOR: FLUENT — trust={trust:.0f}]"
236
- else:
237
- cog_top_k = 3
238
- cog_min_score = 0.65
239
-
240
- query_parts = []
241
- if file_list:
242
- query_parts.append(f"editing files: {', '.join(file_list[:5])}")
243
- if area:
244
- query_parts.append(f"area: {area}")
245
- if query_parts:
246
- query_text = ". ".join(query_parts)
247
- cog_results = cognitive.search(
248
- query_text, top_k=cog_top_k, min_score=cog_min_score,
249
- stores="ltm", source_type_filter="learning", rehearse=False
250
- )
251
- for r in cog_results:
252
- cognitive_warnings.append(
253
- f"[{r['score']:.2f}]: {r['source_title']} — {r['content'][:200]}"
254
- )
255
- except Exception:
256
- pass # Cognitive is optional
257
-
258
- # 8. Somatic markers — risk score per file/area
259
- somatic_risk = 0.0
260
- somatic_details = {}
261
- try:
262
- import cognitive
263
- risk_result = cognitive.somatic_get_risk(file_list, area)
264
- somatic_risk = risk_result["max_risk"]
265
- somatic_details = risk_result["scores"]
266
- # Validated recovery: if no learnings found, guard check is "clean"
267
- if not result["learnings"]:
268
- for fp in file_list:
269
- cognitive.somatic_guard_decay(fp, "file")
270
- except Exception:
271
- pass
272
-
273
- # Record guard hits on learnings (for weight auto-adjustment)
274
- import time
275
- if hit_ids:
276
- for lid in hit_ids:
277
- conn.execute(
278
- "UPDATE learnings SET guard_hits = COALESCE(guard_hits, 0) + 1, last_guard_hit_at = ? WHERE id = ?",
279
- (time.time(), lid)
280
- )
281
-
282
- # Log the guard check
283
- conn.execute(
284
- "INSERT INTO guard_checks (session_id, files, area, learnings_returned, blocking_rules_returned) "
285
- "VALUES (?, ?, ?, ?, ?)",
286
- ("", files, area, len(result["learnings"]) + len(result["universal_rules"]),
287
- len(result["blocking_rules"]))
288
- )
289
- conn.commit()
290
-
291
- # Sort learnings by weight (highest first)
292
- result["learnings"].sort(key=lambda x: x.get("weight", 0.5), reverse=True)
293
-
294
- # Format output
295
- lines = []
296
- if result["blocking_rules"]:
297
- lines.append("BLOCKING RULES (resolve BEFORE writing):")
298
- for r in result["blocking_rules"]:
299
- reason = r.get("reason", "repeated_error")
300
- if reason == "prohibition_keyword":
301
- lines.append(f" #{r['id']} [PROHIBIT]: {r['rule']}")
302
- else:
303
- lines.append(f" #{r['id']} ({r['repetitions']}x repeated): {r['rule']}")
304
- lines.append("")
305
-
306
- if result["learnings"]:
307
- shown = result["learnings"][:10] # Cap at 10, not 15
308
- lines.append(f"RELEVANT LEARNINGS ({len(result['learnings'])}, showing {len(shown)}):")
309
- for l in shown:
310
- lines.append(f" #{l['id']} [{l['category']}] {l['rule']}")
311
- lines.append("")
312
-
313
- if result.get("behavioral_rules"):
314
- lines.append("SESSION BEHAVIORAL RULES (top 5 most-violated):")
315
- for r in result["behavioral_rules"]:
316
- v = f" ({r['violations']}x violated)" if r["violations"] > 0 else ""
317
- lines.append(f" #{r['id']} {r['rule']}{v}")
318
- lines.append("")
319
-
320
- if result["universal_rules"]:
321
- shown_u = result["universal_rules"][:5] # Cap at 5
322
- lines.append(f"UNIVERSAL RULES ({len(result['universal_rules'])}, showing {len(shown_u)}):")
323
- for r in shown_u:
324
- lines.append(f" #{r['id']} {r['rule']}")
325
- lines.append("")
326
-
327
- if result["schemas"]:
328
- lines.append("DB SCHEMAS:")
329
- for table, schema in result["schemas"].items():
330
- lines.append(f" {table}: {schema}")
331
- lines.append("")
332
-
333
- if result["area_repetition_rate"] > 0:
334
- lines.append(f"Area repetition rate: {result['area_repetition_rate']:.0%}")
335
-
336
- if cognitive_warnings:
337
- lines.append(f"\nCOGNITIVE SEMANTIC MATCHES{trust_note}:")
338
- for w in cognitive_warnings:
339
- lines.append(f" COGNITIVE MATCH {w}")
340
-
341
- if somatic_risk > 0:
342
- if somatic_risk > 0.8:
343
- lines.insert(0, "CRITICAL RISK (score {:.2f}) — suggest code review before editing".format(somatic_risk))
344
- elif somatic_risk > 0.5:
345
- lines.insert(0, "HIGH RISK (score {:.2f}) — extra caution recommended".format(somatic_risk))
346
- else:
347
- lines.append("\nSomatic risk: {:.2f} (low)".format(somatic_risk))
348
- if somatic_details:
349
- lines.append("Risk scores:")
350
- for target, data in somatic_details.items():
351
- lines.append(" {}: {:.2f} ({} incidents, last: {})".format(
352
- target, data["risk"], data["incidents"], data["last"][:10] if data["last"] else "unknown"))
353
-
354
- if not lines:
355
- return "No relevant learnings found for these files/area."
356
-
357
- return "\n".join(lines)
358
-
359
-
360
- def handle_guard_stats(period_days: int = 7) -> str:
361
- """Get guard system statistics for the specified period.
362
-
363
- Args:
364
- period_days: Number of days to look back (default 7)
365
- """
366
- conn = get_db()
367
- cutoff = (datetime.now() - timedelta(days=period_days)).strftime("%Y-%m-%d %H:%M:%S")
368
-
369
- total_learnings = conn.execute("SELECT COUNT(*) as cnt FROM learnings").fetchone()["cnt"]
370
-
371
- total_reps = conn.execute(
372
- "SELECT COUNT(*) as cnt FROM error_repetitions WHERE created_at > ?", (cutoff,)
373
- ).fetchone()["cnt"]
374
-
375
- # Repetition rate
376
- new_learnings_period = conn.execute(
377
- "SELECT COUNT(*) as cnt FROM learnings WHERE created_at > ?",
378
- ((datetime.now() - timedelta(days=period_days)).timestamp(),)
379
- ).fetchone()["cnt"]
380
- rep_rate = round(total_reps / new_learnings_period, 2) if new_learnings_period > 0 else 0.0
381
-
382
- # Previous period for trend
383
- prev_cutoff = (datetime.now() - timedelta(days=period_days * 2)).strftime("%Y-%m-%d %H:%M:%S")
384
- prev_reps = conn.execute(
385
- "SELECT COUNT(*) as cnt FROM error_repetitions WHERE created_at > ? AND created_at <= ?",
386
- (prev_cutoff, cutoff)
387
- ).fetchone()["cnt"]
388
- trend = "stable"
389
- if total_reps < prev_reps:
390
- trend = "improving"
391
- elif total_reps > prev_reps:
392
- trend = "worsening"
393
-
394
- # Top areas
395
- area_rows = conn.execute(
396
- "SELECT area, COUNT(*) as cnt FROM error_repetitions WHERE created_at > ? GROUP BY area ORDER BY cnt DESC LIMIT 5",
397
- (cutoff,)
398
- ).fetchall()
399
-
400
- # Most ignored learnings (most repetitions)
401
- ignored_rows = conn.execute(
402
- "SELECT original_learning_id, COUNT(*) as cnt FROM error_repetitions "
403
- "GROUP BY original_learning_id ORDER BY cnt DESC LIMIT 5"
404
- ).fetchall()
405
- most_ignored = []
406
- for r in ignored_rows:
407
- lr = conn.execute("SELECT title FROM learnings WHERE id = ?", (r["original_learning_id"],)).fetchone()
408
- if lr:
409
- most_ignored.append({"id": r["original_learning_id"], "title": lr["title"], "times_repeated": r["cnt"]})
410
-
411
- # Guard checks performed
412
- checks_count = conn.execute(
413
- "SELECT COUNT(*) as cnt FROM guard_checks WHERE created_at > ?", (cutoff,)
414
- ).fetchone()["cnt"]
415
-
416
- lines = [
417
- f"GUARD STATS (last {period_days} days):",
418
- f" Repetition rate: {rep_rate:.0%} ({trend})",
419
- f" Total learnings: {total_learnings}",
420
- f" Repetitions in period: {total_reps}",
421
- f" Guard checks performed: {checks_count}",
422
- ]
423
-
424
- if area_rows:
425
- lines.append(" Top areas:")
426
- for r in area_rows:
427
- lines.append(f" {r['area']}: {r['cnt']} repetitions")
428
-
429
- if most_ignored:
430
- lines.append(" Most repeated learnings:")
431
- for m in most_ignored:
432
- lines.append(f" #{m['id']} ({m['times_repeated']}x): {m['title'][:60]}")
433
-
434
- return "\n".join(lines)
435
-
436
-
437
- def handle_guard_log_repetition(new_learning_id: int, original_learning_id: int, similarity: float = 0.75) -> str:
438
- """Log that a new learning is similar to an existing one (repetition detected).
439
-
440
- Args:
441
- new_learning_id: ID of the new learning
442
- original_learning_id: ID of the original learning it matches
443
- similarity: Similarity score (0-1)
444
- """
445
- conn = get_db()
446
-
447
- # Get the area from the new learning
448
- row = conn.execute("SELECT category FROM learnings WHERE id = ?", (new_learning_id,)).fetchone()
449
- if not row:
450
- return f"ERROR: Learning #{new_learning_id} not found."
451
- area = row["category"]
452
-
453
- conn.execute(
454
- "INSERT INTO error_repetitions (new_learning_id, original_learning_id, similarity, area) VALUES (?,?,?,?)",
455
- (new_learning_id, original_learning_id, similarity, area)
456
- )
457
- conn.commit()
458
-
459
- return f"Repetition logged: #{new_learning_id} similar to #{original_learning_id} ({similarity:.0%})"
460
-
461
-
462
- def handle_somatic_check(files: str = "", area: str = "") -> str:
463
- """View somatic risk scores for specific files and/or area.
464
- Args:
465
- files: Comma-separated file paths to check
466
- area: System area to check
467
- """
468
- try:
469
- import cognitive
470
- file_list = [f.strip() for f in files.split(",") if f.strip()] if files else []
471
- result = cognitive.somatic_get_risk(file_list, area)
472
- if not result["scores"]:
473
- return "No somatic markers found for these targets."
474
- lines = ["Max risk: {:.2f}".format(result["max_risk"]), ""]
475
- for target, data in result["scores"].items():
476
- level = "CRITICAL" if data["risk"] > 0.8 else "HIGH" if data["risk"] > 0.5 else "Low"
477
- lines.append(" {} {}: {:.2f} ({} incidents, last: {})".format(
478
- level, target, data["risk"], data["incidents"], data["last"][:10] if data["last"] else "unknown"))
479
- return "\n".join(lines)
480
- except Exception as e:
481
- return "Error: {}".format(e)
482
-
483
-
484
- def handle_somatic_stats() -> str:
485
- """View top 10 riskiest files/areas and system-wide risk distribution."""
486
- try:
487
- import cognitive
488
- top = cognitive.somatic_top_risks(limit=10)
489
- if not top:
490
- return "No somatic markers recorded yet."
491
- lines = ["TOP RISK TARGETS:", ""]
492
- for r in top:
493
- level = "CRIT" if r["risk_score"] > 0.8 else "HIGH" if r["risk_score"] > 0.5 else "low"
494
- lines.append(" [{}] [{}] {}: {:.2f} ({} incidents)".format(
495
- level, r["target_type"], r["target"], r["risk_score"], r["incident_count"]))
496
- db = cognitive._get_db()
497
- total = db.execute("SELECT COUNT(*) FROM somatic_markers WHERE risk_score > 0").fetchone()[0]
498
- high = db.execute("SELECT COUNT(*) FROM somatic_markers WHERE risk_score > 0.5").fetchone()[0]
499
- critical = db.execute("SELECT COUNT(*) FROM somatic_markers WHERE risk_score > 0.8").fetchone()[0]
500
- lines.extend(["", "Distribution: {} tracked | {} high risk | {} critical".format(total, high, critical)])
501
- return "\n".join(lines)
502
- except Exception as e:
503
- return "Error: {}".format(e)
504
-
505
-
506
- def handle_guard_cross_check(findings: list, area: str = "") -> str:
507
- """Cross-check audit findings against known learnings to filter false positives.
508
-
509
- Args:
510
- findings: List of audit finding strings to cross-check
511
- area: System area to narrow the learning search (webapp, shopify, etc.)
512
- """
513
- # Common English/Spanish stopwords to skip during keyword extraction
514
- STOPWORDS = {
515
- "the", "a", "an", "is", "in", "on", "at", "to", "of", "and", "or", "but",
516
- "for", "with", "that", "this", "it", "as", "are", "was", "be", "by", "not",
517
- "has", "have", "from", "which", "when", "if", "then", "do", "does", "can",
518
- "el", "la", "los", "las", "un", "una", "en", "de", "del", "al", "y", "o",
519
- "que", "se", "no", "es", "por", "con", "su", "pero", "como", "para",
520
- "este", "esta", "esto", "son", "hay", "más", "ya",
521
- }
522
-
523
- new_issues = []
524
- known_issues = []
525
-
526
- for finding in findings:
527
- if not finding or not finding.strip():
528
- continue
529
-
530
- # Extract significant keywords from the finding text
531
- words = finding.lower().split()
532
- keywords = [
533
- w.strip(".,;:!?\"'()[]{}") for w in words
534
- if len(w) >= 4 and w.lower() not in STOPWORDS
535
- ]
536
- # Use up to 5 most distinctive keywords to build the search query
537
- query_keywords = keywords[:5]
538
-
539
- matched_learnings = []
540
- if query_keywords:
541
- query = " ".join(query_keywords)
542
- try:
543
- results = search_learnings(query, category=area if area else None)
544
- if not results and area:
545
- # Retry without category filter if area-filtered search returns nothing
546
- results = search_learnings(query)
547
- matched_learnings = results[:3] # Top 3 matches per finding
548
- except Exception:
549
- pass
550
-
551
- if matched_learnings:
552
- refs = [
553
- {"id": r["id"], "title": r["title"], "category": r.get("category", "")}
554
- for r in matched_learnings
555
- ]
556
- known_issues.append({
557
- "finding": finding,
558
- "status": "known",
559
- "learning_refs": refs,
560
- })
561
- else:
562
- new_issues.append({
563
- "finding": finding,
564
- "status": "new",
565
- })
566
-
567
- # Build output
568
- lines = [
569
- f"CROSS-CHECK RESULTS: {len(findings)} findings — "
570
- f"{len(new_issues)} new, {len(known_issues)} already documented",
571
- "",
572
- ]
573
-
574
- if new_issues:
575
- lines.append(f"NEW ISSUES ({len(new_issues)}) — not in learnings, investigate:")
576
- for i, item in enumerate(new_issues, 1):
577
- lines.append(f" {i}. {item['finding']}")
578
- lines.append("")
579
-
580
- if known_issues:
581
- lines.append(f"KNOWN ISSUES ({len(known_issues)}) — covered by existing learnings:")
582
- for i, item in enumerate(known_issues, 1):
583
- refs_str = ", ".join(
584
- f"#{r['id']} [{r['category']}] {r['title'][:60]}"
585
- for r in item["learning_refs"]
586
- )
587
- lines.append(f" {i}. {item['finding']}")
588
- lines.append(f" -> {refs_str}")
589
- lines.append("")
590
-
591
- summary = {
592
- "total": len(findings),
593
- "new_count": len(new_issues),
594
- "known_count": len(known_issues),
595
- "new_issues": [i["finding"] for i in new_issues],
596
- "known_issues": [
597
- {"finding": i["finding"], "refs": i["learning_refs"]}
598
- for i in known_issues
599
- ],
600
- }
601
- lines.append(f"SUMMARY JSON: {json.dumps(summary)}")
602
-
603
- return "\n".join(lines)
604
-
605
-
606
- def handle_guard_file_check(files: list) -> str:
607
- """Pre-edit check: surfaces learnings and recent changes for files about to be modified.
608
-
609
- Args:
610
- files: List of file paths about to be edited
611
- """
612
- from pathlib import Path
613
- import re
614
-
615
- BLOCKING_KEYWORDS = re.compile(
616
- r'\bNUNCA\b|\bNEVER\b|\bPROHIBIDO\b|\bFORBIDDEN\b|\bBLOCKING\b',
617
- re.IGNORECASE
618
- )
619
-
620
- if not files:
621
- return "ERROR: No files provided."
622
-
623
- file_learnings: dict = {}
624
- recent_changes: dict = {}
625
- warnings: list = []
626
- seen_learning_ids: set = set()
627
-
628
- for filepath in files:
629
- p = Path(filepath)
630
- filename = p.name
631
- parent_dir = p.parent.name
632
- stem = p.stem # filename without extension
633
-
634
- # Build search keywords: filename, stem, parent directory (deduplicated)
635
- keywords = [kw for kw in [filename, stem, parent_dir] if kw and kw not in (".", "")]
636
- seen_kw: set = set()
637
- unique_keywords = []
638
- for kw in keywords:
639
- if kw not in seen_kw:
640
- seen_kw.add(kw)
641
- unique_keywords.append(kw)
642
-
643
- file_results = []
644
- file_seen_ids: set = set()
645
-
646
- for keyword in unique_keywords:
647
- try:
648
- rows = search_learnings(keyword)
649
- for r in rows:
650
- lid = r.get("id")
651
- if lid and lid not in seen_learning_ids and lid not in file_seen_ids:
652
- file_seen_ids.add(lid)
653
- seen_learning_ids.add(lid)
654
- entry = {
655
- "id": lid,
656
- "category": r.get("category", ""),
657
- "title": r.get("title", ""),
658
- "content": (r.get("content") or "")[:300],
659
- }
660
- file_results.append(entry)
661
- # Flag blocking learnings
662
- if BLOCKING_KEYWORDS.search(r.get("title", "")) or \
663
- BLOCKING_KEYWORDS.search(r.get("content") or ""):
664
- warnings.append(
665
- f"[BLOCKING] #{lid} ({filepath}): {r.get('title', '')}"
666
- )
667
- except Exception:
668
- pass
669
-
670
- file_learnings[filepath] = file_results
671
-
672
- # Search recent changes (last 7 days) for this file by filename/stem
673
- file_changes = []
674
- for keyword in unique_keywords[:2]: # filename + stem are most specific
675
- try:
676
- changes = search_changes(files=keyword, days=7)
677
- for c in changes:
678
- cid = c.get("id")
679
- if cid and not any(fc.get("id") == cid for fc in file_changes):
680
- file_changes.append({
681
- "id": cid,
682
- "files": c.get("files", ""),
683
- "what_changed": (c.get("what_changed") or "")[:200],
684
- "why": (c.get("why") or "")[:150],
685
- "created_at": (c.get("created_at") or "")[:16],
686
- })
687
- except Exception:
688
- pass
689
-
690
- recent_changes[filepath] = file_changes
691
-
692
- # Build summary line
693
- total_learnings = sum(len(v) for v in file_learnings.values())
694
- total_changes = sum(len(v) for v in recent_changes.values())
695
- summary_parts = []
696
- if total_learnings:
697
- summary_parts.append(f"{total_learnings} learning(s) found")
698
- if total_changes:
699
- summary_parts.append(f"{total_changes} recent change(s) in last 7 days")
700
- if warnings:
701
- summary_parts.append(f"{len(warnings)} BLOCKING warning(s)")
702
- summary = ", ".join(summary_parts) if summary_parts else "No relevant learnings or recent changes found."
703
-
704
- # Format output
705
- lines = []
706
-
707
- if warnings:
708
- lines.append("WARNINGS — resolve before editing:")
709
- for w in warnings:
710
- lines.append(f" {w}")
711
- lines.append("")
712
-
713
- for filepath in files:
714
- learnings = file_learnings.get(filepath, [])
715
- changes = recent_changes.get(filepath, [])
716
- if not learnings and not changes:
717
- continue
718
- lines.append(f"FILE: {filepath}")
719
- if learnings:
720
- lines.append(f" Learnings ({len(learnings)}):")
721
- for entry in learnings[:10]:
722
- lines.append(f" #{entry['id']} [{entry['category']}] {entry['title']}")
723
- if entry["content"]:
724
- lines.append(f" {entry['content'][:120]}")
725
- if changes:
726
- lines.append(f" Recent changes ({len(changes)}, last 7d):")
727
- for c in changes[:5]:
728
- lines.append(f" [{c['created_at']}] {c['what_changed'][:100]}")
729
- if c["why"]:
730
- lines.append(f" Why: {c['why'][:80]}")
731
- lines.append("")
732
-
733
- lines.append(f"SUMMARY: {summary}")
734
-
735
- return "\n".join(lines) if lines else summary
736
-
737
-
738
- TOOLS = [
739
- (handle_guard_check, "nexo_guard_check", "Check learnings relevant to files/area BEFORE editing code. Call this before any code change."),
740
- (handle_guard_stats, "nexo_guard_stats", "Get guard system statistics: repetition rate, trends, top problem areas"),
741
- (handle_guard_log_repetition, "nexo_guard_log_repetition", "Log a learning repetition (new learning matches existing one)"),
742
- (handle_somatic_check, "nexo_somatic_check", "View somatic risk scores for files/areas — pain memory"),
743
- (handle_somatic_stats, "nexo_somatic_stats", "Top 10 riskiest targets + risk distribution"),
744
- (handle_guard_cross_check, "nexo_guard_cross_check", "Cross-check audit findings against known learnings to filter false positives"),
745
- (handle_guard_file_check, "nexo_guard_file_check", "Pre-edit check: surfaces learnings and recent changes for files about to be modified"),
746
- ]