nexo-brain 2.6.21 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +72 -20
- package/hooks/hooks.json +79 -0
- package/package.json +1 -1
- package/src/agent_runner.py +296 -8
- package/src/cli.py +209 -4
- package/src/client_preferences.py +115 -0
- package/src/client_sync.py +202 -2
- package/src/cognitive/__init__.py +1 -1
- package/src/cognitive/_search.py +39 -19
- package/src/dashboard/app.py +264 -0
- package/src/dashboard/templates/base.html +4 -0
- package/src/dashboard/templates/dashboard.html +59 -1
- package/src/dashboard/templates/protocol.html +199 -0
- package/src/db/__init__.py +23 -1
- package/src/db/_learnings.py +31 -4
- package/src/db/_personal_scripts.py +12 -0
- package/src/db/_protocol.py +303 -0
- package/src/db/_schema.py +248 -0
- package/src/db/_watchers.py +173 -0
- package/src/db/_workflow.py +952 -0
- package/src/doctor/providers/runtime.py +1095 -3
- package/src/evolution_cycle.py +62 -0
- package/src/hook_guardrails.py +308 -0
- package/src/hooks/protocol-guardrail.sh +10 -0
- package/src/nexo_sdk.py +103 -0
- package/src/plugins/cognitive_memory.py +18 -0
- package/src/plugins/cortex.py +55 -35
- package/src/plugins/guard.py +132 -16
- package/src/plugins/protocol.py +911 -0
- package/src/plugins/schedule.py +40 -6
- package/src/plugins/simple_api.py +103 -0
- package/src/plugins/skills.py +67 -0
- package/src/plugins/state_watchers.py +79 -0
- package/src/plugins/workflow.py +588 -0
- package/src/public_contribution.py +86 -12
- package/src/script_registry.py +142 -0
- package/src/scripts/deep-sleep/apply_findings.py +482 -2
- package/src/scripts/deep-sleep/collect.py +49 -4
- package/src/scripts/nexo-agent-run.py +2 -0
- package/src/scripts/nexo-daily-self-audit.py +843 -5
- package/src/scripts/nexo-evolution-run.py +343 -1
- package/src/server.py +92 -6
- package/src/skills_runtime.py +151 -0
- package/src/state_watchers_runtime.py +334 -0
- package/src/tools_learnings.py +345 -7
- package/src/tools_sessions.py +183 -0
- package/templates/CLAUDE.md.template +9 -1
- package/templates/CODEX.AGENTS.md.template +10 -2
package/src/plugins/guard.py
CHANGED
|
@@ -5,10 +5,69 @@ and provides stats on error prevention effectiveness.
|
|
|
5
5
|
"""
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
from datetime import datetime, timedelta
|
|
10
|
+
from pathlib import Path
|
|
9
11
|
from db import get_db, find_similar_learnings, extract_keywords, search_learnings, search_changes
|
|
10
12
|
|
|
11
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
|
+
|
|
12
71
|
|
|
13
72
|
def _load_schema_cache() -> dict:
|
|
14
73
|
"""Load cached DB schemas from schema_cache.json."""
|
|
@@ -69,17 +128,49 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
69
128
|
result = {
|
|
70
129
|
"learnings": [],
|
|
71
130
|
"universal_rules": [],
|
|
131
|
+
"conditioned_learnings": [],
|
|
72
132
|
"schemas": {},
|
|
73
133
|
"area_repetition_rate": 0.0,
|
|
74
134
|
"blocking_rules": [],
|
|
75
135
|
}
|
|
76
136
|
|
|
77
137
|
seen_ids = set()
|
|
138
|
+
conditioned_blocking_seen = set()
|
|
139
|
+
conditioned_by_file = _load_conditioned_learnings(conn, file_list) if file_list else {}
|
|
78
140
|
|
|
79
|
-
# 1.
|
|
141
|
+
# 1. File-conditioned learnings — explicit applies_to guardrails for target files
|
|
80
142
|
hit_ids = []
|
|
81
143
|
for filepath in file_list:
|
|
82
|
-
|
|
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:
|
|
83
174
|
p = Path(filepath)
|
|
84
175
|
filename = p.name
|
|
85
176
|
parent_dir = p.parent.name
|
|
@@ -96,7 +187,7 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
96
187
|
w = r["weight"] or 0.5
|
|
97
188
|
result["learnings"].append({"id": r["id"], "category": r["category"], "rule": r["title"], "priority": pri, "weight": w})
|
|
98
189
|
|
|
99
|
-
#
|
|
190
|
+
# 3. By area/category
|
|
100
191
|
if area:
|
|
101
192
|
rows = conn.execute(
|
|
102
193
|
"SELECT id, category, title, content, priority, weight FROM learnings WHERE category = ?",
|
|
@@ -110,14 +201,14 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
110
201
|
w = r["weight"] or 0.5
|
|
111
202
|
result["learnings"].append({"id": r["id"], "category": r["category"], "rule": r["title"], "priority": pri, "weight": w})
|
|
112
203
|
|
|
113
|
-
#
|
|
204
|
+
# 4. Universal rules — only from matching area or nexo-ops (not ALL learnings)
|
|
114
205
|
universal_categories = {"nexo-ops"}
|
|
115
206
|
if area:
|
|
116
207
|
universal_categories.add(area)
|
|
117
208
|
placeholders = ",".join("?" for _ in universal_categories)
|
|
118
209
|
rows = conn.execute(
|
|
119
210
|
f"SELECT id, category, title, content, priority FROM learnings WHERE "
|
|
120
|
-
f"category IN ({placeholders}) AND ("
|
|
211
|
+
f"category IN ({placeholders}) AND COALESCE(applies_to, '') = '' AND ("
|
|
121
212
|
f"content LIKE '%SIEMPRE%' OR content LIKE '%NUNCA%' OR content LIKE '%ANTES%' "
|
|
122
213
|
f"OR content LIKE '%always%' OR content LIKE '%never%')",
|
|
123
214
|
tuple(universal_categories)
|
|
@@ -127,7 +218,7 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
127
218
|
seen_ids.add(r["id"])
|
|
128
219
|
result["universal_rules"].append({"id": r["id"], "rule": r["title"], "category": r["category"], "priority": r["priority"] or "medium"})
|
|
129
220
|
|
|
130
|
-
#
|
|
221
|
+
# 5. DB schemas if files contain SQL keywords
|
|
131
222
|
if include_schemas_bool and file_list:
|
|
132
223
|
all_tables = set()
|
|
133
224
|
for filepath in file_list:
|
|
@@ -149,7 +240,7 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
149
240
|
elif "cloud_sql" in cache and table in cache["cloud_sql"]:
|
|
150
241
|
result["schemas"][table] = cache["cloud_sql"][table]
|
|
151
242
|
|
|
152
|
-
#
|
|
243
|
+
# 6. Check for blocking rules — two paths:
|
|
153
244
|
# (a) 5+ repetitions (existing behavior)
|
|
154
245
|
# (b) Learning contains NUNCA/NEVER/PROHIBIDO and matches semantically (aggressive mode)
|
|
155
246
|
import re
|
|
@@ -160,7 +251,7 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
160
251
|
# Check both learnings and universal_rules for blocking
|
|
161
252
|
all_candidates = [(l, "learning") for l in result["learnings"]] + \
|
|
162
253
|
[(u, "universal") for u in result["universal_rules"]]
|
|
163
|
-
blocking_seen = set()
|
|
254
|
+
blocking_seen = set(conditioned_blocking_seen)
|
|
164
255
|
for learning, source in all_candidates:
|
|
165
256
|
lid = learning["id"]
|
|
166
257
|
if lid in blocking_seen:
|
|
@@ -188,7 +279,7 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
188
279
|
"reason": "prohibition_keyword"
|
|
189
280
|
})
|
|
190
281
|
|
|
191
|
-
#
|
|
282
|
+
# 6b. Behavioral rules — when called without files (session-level check)
|
|
192
283
|
if not file_list:
|
|
193
284
|
behavioral = conn.execute(
|
|
194
285
|
"""SELECT l.id, l.title, l.category, COUNT(e.id) as violations
|
|
@@ -205,7 +296,7 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
205
296
|
for r in behavioral
|
|
206
297
|
]
|
|
207
298
|
|
|
208
|
-
#
|
|
299
|
+
# 7. Area repetition rate
|
|
209
300
|
if area:
|
|
210
301
|
total_area = conn.execute(
|
|
211
302
|
"SELECT COUNT(*) as cnt FROM learnings WHERE category = ?", (area,)
|
|
@@ -216,7 +307,7 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
216
307
|
if total_area > 0:
|
|
217
308
|
result["area_repetition_rate"] = round(reps_area / total_area, 2)
|
|
218
309
|
|
|
219
|
-
#
|
|
310
|
+
# 8. Cognitive metacognition — semantic search for related warnings
|
|
220
311
|
# Trust score modulates rigor: <40 = paranoid mode (more results, lower threshold)
|
|
221
312
|
cognitive_warnings = []
|
|
222
313
|
trust_note = ""
|
|
@@ -255,7 +346,7 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
255
346
|
except Exception:
|
|
256
347
|
pass # Cognitive is optional
|
|
257
348
|
|
|
258
|
-
#
|
|
349
|
+
# 9. Somatic markers — risk score per file/area
|
|
259
350
|
somatic_risk = 0.0
|
|
260
351
|
somatic_details = {}
|
|
261
352
|
try:
|
|
@@ -297,12 +388,20 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
|
|
|
297
388
|
lines.append("BLOCKING RULES (resolve BEFORE writing):")
|
|
298
389
|
for r in result["blocking_rules"]:
|
|
299
390
|
reason = r.get("reason", "repeated_error")
|
|
300
|
-
if reason == "
|
|
391
|
+
if reason == "file_conditioned":
|
|
392
|
+
lines.append(f" #{r['id']} [FILE RULE:{r.get('file', '')}]: {r['rule']}")
|
|
393
|
+
elif reason == "prohibition_keyword":
|
|
301
394
|
lines.append(f" #{r['id']} [PROHIBIT]: {r['rule']}")
|
|
302
395
|
else:
|
|
303
396
|
lines.append(f" #{r['id']} ({r['repetitions']}x repeated): {r['rule']}")
|
|
304
397
|
lines.append("")
|
|
305
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
|
+
|
|
306
405
|
if result["learnings"]:
|
|
307
406
|
shown = result["learnings"][:10] # Cap at 10, not 15
|
|
308
407
|
lines.append(f"RELEVANT LEARNINGS ({len(result['learnings'])}, showing {len(shown)}):")
|
|
@@ -609,9 +708,6 @@ def handle_guard_file_check(files: list) -> str:
|
|
|
609
708
|
Args:
|
|
610
709
|
files: List of file paths about to be edited
|
|
611
710
|
"""
|
|
612
|
-
from pathlib import Path
|
|
613
|
-
import re
|
|
614
|
-
|
|
615
711
|
BLOCKING_KEYWORDS = re.compile(
|
|
616
712
|
r'\bNUNCA\b|\bNEVER\b|\bPROHIBIDO\b|\bFORBIDDEN\b|\bBLOCKING\b',
|
|
617
713
|
re.IGNORECASE
|
|
@@ -624,6 +720,8 @@ def handle_guard_file_check(files: list) -> str:
|
|
|
624
720
|
recent_changes: dict = {}
|
|
625
721
|
warnings: list = []
|
|
626
722
|
seen_learning_ids: set = set()
|
|
723
|
+
conn = get_db()
|
|
724
|
+
conditioned_by_file = _load_conditioned_learnings(conn, files)
|
|
627
725
|
|
|
628
726
|
for filepath in files:
|
|
629
727
|
p = Path(filepath)
|
|
@@ -643,10 +741,28 @@ def handle_guard_file_check(files: list) -> str:
|
|
|
643
741
|
file_results = []
|
|
644
742
|
file_seen_ids: set = set()
|
|
645
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
|
+
|
|
646
759
|
for keyword in unique_keywords:
|
|
647
760
|
try:
|
|
648
761
|
rows = search_learnings(keyword)
|
|
649
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
|
|
650
766
|
lid = r.get("id")
|
|
651
767
|
if lid and lid not in seen_learning_ids and lid not in file_seen_ids:
|
|
652
768
|
file_seen_ids.add(lid)
|