nexo-brain 5.3.26 → 5.3.28

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