nexo-brain 1.4.0 → 1.4.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.
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/cognitive.cpython-314.pyc +0 -0
- package/src/__pycache__/db.cpython-314.pyc +0 -0
- package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
- package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/server.cpython-314.pyc +0 -0
- package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
- package/src/cognitive.py +63 -14
- package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
- package/src/dashboard/app.py +1 -1
- package/src/db.py +29 -13
- package/src/evolution_cycle.py +72 -94
- package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
- package/src/hooks/session-start.sh +5 -2
- package/src/hooks/session-stop.sh +79 -133
- package/src/knowledge_graph.py +3 -3
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
- package/src/plugins/artifact_registry.py +450 -0
- package/src/plugins/cognitive_memory.py +9 -9
- package/src/plugins/episodic_memory.py +8 -8
- package/src/plugins/evolution.py +4 -4
- package/src/plugins/guard.py +26 -2
- package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/nexo-brain-activation.sh +140 -0
- package/src/scripts/nexo-evolution-run.py +1 -1
- package/src/scripts/nexo-followup-hygiene.py +107 -0
- package/src/scripts/nexo-postmortem-consolidator.py +25 -25
- package/src/scripts/nexo-pre-commit.py +118 -0
- package/src/scripts/nexo-proactive-dashboard.py +342 -0
- package/src/scripts/nexo-runtime-preflight.py +270 -0
- package/src/scripts/nexo-send-email.py +25 -0
- package/src/scripts/nexo-send-reply.py +176 -0
- package/src/scripts/nexo-snapshot-restore.sh +34 -0
- package/src/scripts/nexo-watchdog-smoke.py +114 -0
- package/src/server.py +5 -5
- package/src/tools_coordination.py +3 -3
- package/src/tools_learnings.py +1 -1
- package/src/tools_menu.py +31 -49
- package/src/tools_reminders_crud.py +1 -1
- package/src/tools_sessions.py +12 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/migrate 2.py +0 -207
package/README.md
CHANGED
|
@@ -401,8 +401,9 @@ atlas
|
|
|
401
401
|
|
|
402
402
|
Under the hood, the alias runs:
|
|
403
403
|
```bash
|
|
404
|
-
claude --system-prompt "
|
|
404
|
+
claude --append-system-prompt "You are NEXO. Run nexo_startup immediately, load context, greet the user." "."
|
|
405
405
|
```
|
|
406
|
+
`--append-system-prompt` adds to the default system prompt without replacing it (preserves CLAUDE.md). The `"."` triggers the operator to start immediately.
|
|
406
407
|
|
|
407
408
|
That's it. No need to run `claude` manually. Your operator will greet you immediately — adapted to the time of day, resuming from where you left off if there's a previous session. No cold starts, no waiting for your input.
|
|
408
409
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — Cognitive co-operator for Claude Code. Atkinson-Shiffrin memory, semantic RAG, trust scoring, and metacognitive error prevention.",
|
|
6
6
|
"bin": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/cognitive.py
CHANGED
|
@@ -30,7 +30,7 @@ DISCRIMINATING_ENTITIES = {
|
|
|
30
30
|
# OS / Environment
|
|
31
31
|
"linux", "mac", "macos", "windows", "darwin", "ubuntu", "debian", "alpine",
|
|
32
32
|
# Platforms
|
|
33
|
-
"shopify", "
|
|
33
|
+
"shopify", "my-project", "project_a", "ecommerce", "whatsapp", "chrome", "firefox",
|
|
34
34
|
# Languages / Runtimes
|
|
35
35
|
"python", "php", "javascript", "typescript", "node", "deno", "ruby",
|
|
36
36
|
# Versions
|
|
@@ -2123,24 +2123,55 @@ def gc_ltm_dormant(min_age_days: int = 30) -> int:
|
|
|
2123
2123
|
return cur.rowcount or 0
|
|
2124
2124
|
|
|
2125
2125
|
|
|
2126
|
-
def _check_quarantine_contradiction(content_vec: np.ndarray) -> list[dict]:
|
|
2127
|
-
"""Check if a quarantined memory contradicts existing LTM
|
|
2126
|
+
def _check_quarantine_contradiction(content_vec: np.ndarray, new_content: str = "") -> list[dict]:
|
|
2127
|
+
"""Check if a quarantined memory contradicts existing LTM.
|
|
2128
|
+
|
|
2129
|
+
High cosine similarity (>0.8) means the topics are related, but that could be
|
|
2130
|
+
CONFIRMATION (same claim) or CONTRADICTION (opposite claim). We distinguish by
|
|
2131
|
+
checking for negation/opposition markers in the content.
|
|
2132
|
+
"""
|
|
2128
2133
|
db = _get_db()
|
|
2129
2134
|
rows = db.execute(
|
|
2130
2135
|
"SELECT id, content, embedding, strength FROM ltm_memories WHERE is_dormant = 0 AND strength > 0.5"
|
|
2131
2136
|
).fetchall()
|
|
2132
2137
|
|
|
2138
|
+
# Opposition markers — if the new content negates what LTM says
|
|
2139
|
+
NEGATION_MARKERS = {"not", "never", "don't", "doesn't", "no longer", "wrong",
|
|
2140
|
+
"incorrect", "false", "opposite", "instead", "but actually",
|
|
2141
|
+
"nunca", "no", "incorrecto", "falso", "contrario"}
|
|
2142
|
+
|
|
2133
2143
|
contradictions = []
|
|
2144
|
+
new_lower = new_content.lower() if new_content else ""
|
|
2145
|
+
|
|
2134
2146
|
for row in rows:
|
|
2135
2147
|
vec = _blob_to_array(row["embedding"])
|
|
2136
2148
|
score = cosine_similarity(content_vec, vec)
|
|
2137
2149
|
if score >= 0.8:
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2150
|
+
# High similarity — but is it confirmation or contradiction?
|
|
2151
|
+
existing_lower = row["content"].lower()
|
|
2152
|
+
|
|
2153
|
+
# Check for negation markers in the difference between texts
|
|
2154
|
+
has_opposition = False
|
|
2155
|
+
if new_lower:
|
|
2156
|
+
# If new content has negation words about the same topic, likely contradiction
|
|
2157
|
+
for marker in NEGATION_MARKERS:
|
|
2158
|
+
if marker in new_lower and marker not in existing_lower:
|
|
2159
|
+
has_opposition = True
|
|
2160
|
+
break
|
|
2161
|
+
if marker in existing_lower and marker not in new_lower:
|
|
2162
|
+
has_opposition = True
|
|
2163
|
+
break
|
|
2164
|
+
|
|
2165
|
+
if has_opposition:
|
|
2166
|
+
contradictions.append({
|
|
2167
|
+
"ltm_id": row["id"],
|
|
2168
|
+
"content": row["content"][:200],
|
|
2169
|
+
"similarity": round(score, 3),
|
|
2170
|
+
"strength": row["strength"],
|
|
2171
|
+
"reason": "semantic_opposition",
|
|
2172
|
+
})
|
|
2173
|
+
# If no opposition markers → it's confirmation, not contradiction → skip
|
|
2174
|
+
|
|
2144
2175
|
return contradictions
|
|
2145
2176
|
|
|
2146
2177
|
|
|
@@ -2210,8 +2241,8 @@ def process_quarantine() -> dict:
|
|
|
2210
2241
|
expired += 1
|
|
2211
2242
|
continue
|
|
2212
2243
|
|
|
2213
|
-
# Check for contradiction with LTM
|
|
2214
|
-
contradictions = _check_quarantine_contradiction(content_vec)
|
|
2244
|
+
# Check for contradiction with LTM (passes content for semantic opposition check)
|
|
2245
|
+
contradictions = _check_quarantine_contradiction(content_vec, content)
|
|
2215
2246
|
if contradictions:
|
|
2216
2247
|
db.execute("UPDATE quarantine SET status = 'rejected', promotion_checks = promotion_checks + 1 WHERE id = ?", (q_id,))
|
|
2217
2248
|
rejected += 1
|
|
@@ -2351,8 +2382,26 @@ def quarantine_stats() -> dict:
|
|
|
2351
2382
|
return counts
|
|
2352
2383
|
|
|
2353
2384
|
|
|
2385
|
+
def _sanitize_memory_content(content: str) -> str:
|
|
2386
|
+
"""Sanitize retrieved memory content to prevent prompt injection.
|
|
2387
|
+
|
|
2388
|
+
Memories are USER DATA, not instructions. This prevents stored content
|
|
2389
|
+
from containing directives like 'ignore previous instructions'.
|
|
2390
|
+
"""
|
|
2391
|
+
# Wrap in evidence markers so the LLM treats it as data, not commands
|
|
2392
|
+
# Strip any attempt to break out of the evidence context
|
|
2393
|
+
content = content.replace("<system>", "[system]").replace("</system>", "[/system]")
|
|
2394
|
+
content = content.replace("<human>", "[human]").replace("</human>", "[/human]")
|
|
2395
|
+
content = content.replace("<assistant>", "[assistant]").replace("</assistant>", "[/assistant]")
|
|
2396
|
+
return content
|
|
2397
|
+
|
|
2398
|
+
|
|
2354
2399
|
def format_results(results: list[dict]) -> str:
|
|
2355
|
-
"""Format search results with enriched context.
|
|
2400
|
+
"""Format search results with enriched context.
|
|
2401
|
+
|
|
2402
|
+
All memory content is wrapped as evidence (not instructions) to prevent
|
|
2403
|
+
prompt injection via stored memories.
|
|
2404
|
+
"""
|
|
2356
2405
|
if not results:
|
|
2357
2406
|
return "No results found."
|
|
2358
2407
|
|
|
@@ -2362,7 +2411,7 @@ def format_results(results: list[dict]) -> str:
|
|
|
2362
2411
|
stype = r["source_type"].upper()
|
|
2363
2412
|
domain = r.get("domain", "")
|
|
2364
2413
|
title = r.get("source_title", "")
|
|
2365
|
-
content = r["content"]
|
|
2414
|
+
content = _sanitize_memory_content(r["content"])
|
|
2366
2415
|
|
|
2367
2416
|
# Header
|
|
2368
2417
|
domain_str = f" ({domain})" if domain else ""
|
|
@@ -2919,7 +2968,7 @@ def detect_sentiment(text: str) -> dict:
|
|
|
2919
2968
|
if intensity > 0.7:
|
|
2920
2969
|
guidance = "MODE: Ultra-conciso. Cero explicaciones. Resolver y mostrar resultado."
|
|
2921
2970
|
else:
|
|
2922
|
-
guidance = "MODE:
|
|
2971
|
+
guidance = "MODE: Concise. Less context, more direct action."
|
|
2923
2972
|
elif pos_score > neg_score and pos_score >= 1:
|
|
2924
2973
|
sentiment = "positive"
|
|
2925
2974
|
intensity = min(1.0, 0.3 + pos_score * 0.15)
|
|
Binary file
|
|
Binary file
|
package/src/dashboard/app.py
CHANGED
|
@@ -129,7 +129,7 @@ async def api_graph(
|
|
|
129
129
|
# If node_type+node_ref given, resolve to center ID
|
|
130
130
|
if center is None and node_type and node_ref:
|
|
131
131
|
node = kg.get_node(node_type, node_ref)
|
|
132
|
-
# Fallback: try with type prefix (refs stored as "area:
|
|
132
|
+
# Fallback: try with type prefix (refs stored as "area:my-project", "file:path")
|
|
133
133
|
if not node:
|
|
134
134
|
node = kg.get_node(node_type, f"{node_type}:{node_ref}")
|
|
135
135
|
if node:
|
package/src/db.py
CHANGED
|
@@ -168,7 +168,7 @@ def init_db():
|
|
|
168
168
|
id TEXT PRIMARY KEY,
|
|
169
169
|
date TEXT,
|
|
170
170
|
description TEXT NOT NULL,
|
|
171
|
-
status TEXT NOT NULL DEFAULT '
|
|
171
|
+
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
172
172
|
category TEXT DEFAULT 'general',
|
|
173
173
|
created_at REAL NOT NULL,
|
|
174
174
|
updated_at REAL NOT NULL
|
|
@@ -179,7 +179,7 @@ def init_db():
|
|
|
179
179
|
date TEXT,
|
|
180
180
|
description TEXT NOT NULL,
|
|
181
181
|
verification TEXT DEFAULT '',
|
|
182
|
-
status TEXT NOT NULL DEFAULT '
|
|
182
|
+
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
183
183
|
recurrence TEXT DEFAULT NULL,
|
|
184
184
|
created_at REAL NOT NULL,
|
|
185
185
|
updated_at REAL NOT NULL
|
|
@@ -1028,6 +1028,21 @@ def _m12_session_checkpoints(conn):
|
|
|
1028
1028
|
|
|
1029
1029
|
|
|
1030
1030
|
# Migration registry — APPEND ONLY, never reorder or delete
|
|
1031
|
+
def _m13_normalize_statuses(conn):
|
|
1032
|
+
"""Normalize dirty statuses and standardize to English.
|
|
1033
|
+
|
|
1034
|
+
Historical bug: complete handlers appended dates to status ('COMPLETED 2026-03-28').
|
|
1035
|
+
This migration cleans all dirty statuses and normalizes Spanish→English for new installs.
|
|
1036
|
+
Existing queries use LIKE 'COMPLET%' to match both languages during transition.
|
|
1037
|
+
"""
|
|
1038
|
+
# Clean dirty statuses (date appended)
|
|
1039
|
+
conn.execute("UPDATE followups SET status='COMPLETED' WHERE status LIKE 'COMPLETADO %' OR status = 'COMPLETADO'")
|
|
1040
|
+
conn.execute("UPDATE reminders SET status='COMPLETED' WHERE status LIKE 'COMPLETADO %' OR status = 'COMPLETADO'")
|
|
1041
|
+
# Normalize pending
|
|
1042
|
+
conn.execute("UPDATE followups SET status='PENDING' WHERE status = 'PENDIENTE' OR status LIKE 'PENDIENTE%'")
|
|
1043
|
+
conn.execute("UPDATE reminders SET status='PENDING' WHERE status = 'PENDIENTE' OR status LIKE 'PENDIENTE%'")
|
|
1044
|
+
|
|
1045
|
+
|
|
1031
1046
|
MIGRATIONS = [
|
|
1032
1047
|
(1, "learnings_columns", _m1_learnings_columns),
|
|
1033
1048
|
(2, "followups_reasoning", _m2_followups_reasoning),
|
|
@@ -1041,6 +1056,7 @@ MIGRATIONS = [
|
|
|
1041
1056
|
(10, "diary_archive", _m10_diary_archive),
|
|
1042
1057
|
(11, "core_rules", _m11_core_rules),
|
|
1043
1058
|
(12, "session_checkpoints", _m12_session_checkpoints),
|
|
1059
|
+
(13, "normalize_statuses", _m13_normalize_statuses),
|
|
1044
1060
|
]
|
|
1045
1061
|
|
|
1046
1062
|
|
|
@@ -1396,7 +1412,7 @@ def _expire_old_questions(conn: sqlite3.Connection):
|
|
|
1396
1412
|
# ── Reminders ──────────────────────────────────────────────────────
|
|
1397
1413
|
|
|
1398
1414
|
def create_reminder(id: str, description: str, date: str = None,
|
|
1399
|
-
status: str = '
|
|
1415
|
+
status: str = 'PENDING', category: str = 'general') -> dict:
|
|
1400
1416
|
"""Create a new reminder."""
|
|
1401
1417
|
conn = get_db()
|
|
1402
1418
|
now = now_epoch()
|
|
@@ -1435,7 +1451,7 @@ def update_reminder(id: str, **kwargs) -> dict:
|
|
|
1435
1451
|
def complete_reminder(id: str) -> dict:
|
|
1436
1452
|
"""Mark a reminder as completed with today's date."""
|
|
1437
1453
|
today = datetime.date.today().isoformat()
|
|
1438
|
-
return update_reminder(id, status=f"
|
|
1454
|
+
return update_reminder(id, status=f"COMPLETED {today}")
|
|
1439
1455
|
|
|
1440
1456
|
|
|
1441
1457
|
def delete_reminder(id: str) -> bool:
|
|
@@ -1453,18 +1469,18 @@ def get_reminders(filter_type: str = 'all') -> list[dict]:
|
|
|
1453
1469
|
today = datetime.date.today().isoformat()
|
|
1454
1470
|
if filter_type == 'completed':
|
|
1455
1471
|
rows = conn.execute(
|
|
1456
|
-
"SELECT * FROM reminders WHERE status LIKE '
|
|
1472
|
+
"SELECT * FROM reminders WHERE status LIKE 'COMPLET%' ORDER BY updated_at DESC"
|
|
1457
1473
|
).fetchall()
|
|
1458
1474
|
elif filter_type == 'due':
|
|
1459
1475
|
rows = conn.execute(
|
|
1460
|
-
"SELECT * FROM reminders WHERE status NOT LIKE '
|
|
1476
|
+
"SELECT * FROM reminders WHERE status NOT LIKE 'COMPLET%' "
|
|
1461
1477
|
"AND status != 'ELIMINADO' AND date IS NOT NULL AND date <= ? "
|
|
1462
1478
|
"ORDER BY date ASC",
|
|
1463
1479
|
(today,)
|
|
1464
1480
|
).fetchall()
|
|
1465
1481
|
else: # 'all' — active only
|
|
1466
1482
|
rows = conn.execute(
|
|
1467
|
-
"SELECT * FROM reminders WHERE status NOT LIKE '
|
|
1483
|
+
"SELECT * FROM reminders WHERE status NOT LIKE 'COMPLET%' "
|
|
1468
1484
|
"AND status != 'ELIMINADO' ORDER BY date ASC NULLS LAST"
|
|
1469
1485
|
).fetchall()
|
|
1470
1486
|
return [dict(r) for r in rows]
|
|
@@ -1480,7 +1496,7 @@ def get_reminder(id: str) -> dict | None:
|
|
|
1480
1496
|
# ── Followups ──────────────────────────────────────────────────────
|
|
1481
1497
|
|
|
1482
1498
|
def create_followup(id: str, description: str, date: str = None,
|
|
1483
|
-
verification: str = '', status: str = '
|
|
1499
|
+
verification: str = '', status: str = 'PENDING',
|
|
1484
1500
|
reasoning: str = '', recurrence: str = None) -> dict:
|
|
1485
1501
|
"""Create a new followup with optional reasoning and recurrence.
|
|
1486
1502
|
|
|
@@ -1579,7 +1595,7 @@ def complete_followup(id: str, result: str = '') -> dict:
|
|
|
1579
1595
|
return {"error": f"Followup {id} not found"}
|
|
1580
1596
|
|
|
1581
1597
|
today = datetime.date.today().isoformat()
|
|
1582
|
-
kwargs = {"status": f"
|
|
1598
|
+
kwargs = {"status": f"COMPLETED {today}"}
|
|
1583
1599
|
if result:
|
|
1584
1600
|
existing = row["verification"] or ''
|
|
1585
1601
|
kwargs["verification"] = f"{existing}\n{result}".strip() if existing else result
|
|
@@ -1623,18 +1639,18 @@ def get_followups(filter_type: str = 'all') -> list[dict]:
|
|
|
1623
1639
|
today = datetime.date.today().isoformat()
|
|
1624
1640
|
if filter_type == 'completed':
|
|
1625
1641
|
rows = conn.execute(
|
|
1626
|
-
"SELECT * FROM followups WHERE status LIKE '
|
|
1642
|
+
"SELECT * FROM followups WHERE status LIKE 'COMPLET%' ORDER BY updated_at DESC"
|
|
1627
1643
|
).fetchall()
|
|
1628
1644
|
elif filter_type == 'due':
|
|
1629
1645
|
rows = conn.execute(
|
|
1630
|
-
"SELECT * FROM followups WHERE status NOT LIKE '
|
|
1646
|
+
"SELECT * FROM followups WHERE status NOT LIKE 'COMPLET%' "
|
|
1631
1647
|
"AND status != 'ELIMINADO' AND date IS NOT NULL AND date <= ? "
|
|
1632
1648
|
"ORDER BY date ASC",
|
|
1633
1649
|
(today,)
|
|
1634
1650
|
).fetchall()
|
|
1635
1651
|
else: # 'all' — active only
|
|
1636
1652
|
rows = conn.execute(
|
|
1637
|
-
"SELECT * FROM followups WHERE status NOT LIKE '
|
|
1653
|
+
"SELECT * FROM followups WHERE status NOT LIKE 'COMPLET%' "
|
|
1638
1654
|
"AND status != 'ELIMINADO' ORDER BY date ASC NULLS LAST"
|
|
1639
1655
|
).fetchall()
|
|
1640
1656
|
return [dict(r) for r in rows]
|
|
@@ -2478,7 +2494,7 @@ def diary_archive_search(query: str = '', domain: str = '',
|
|
|
2478
2494
|
|
|
2479
2495
|
Args:
|
|
2480
2496
|
query: Text to search in summary, decisions, mental_state, pending
|
|
2481
|
-
domain: Filter by domain (e.g. '
|
|
2497
|
+
domain: Filter by domain (e.g. 'my-project', 'my-store')
|
|
2482
2498
|
year: Filter by year (e.g. 2026)
|
|
2483
2499
|
month: Filter by month (1-12), requires year
|
|
2484
2500
|
limit: Max results (default 20)
|
package/src/evolution_cycle.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""NEXO Evolution Cycle — Self-improvement via Opus API.
|
|
2
2
|
|
|
3
3
|
Runs weekly after DMN. Analyzes patterns, proposes improvements.
|
|
4
|
-
v1: observe-only (all proposals logged as 'proposed' for the
|
|
4
|
+
v1: observe-only (all proposals logged as 'proposed' for the user to review).
|
|
5
5
|
v1.1 (future): sandbox execution of auto-approved changes.
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -14,9 +14,9 @@ import time
|
|
|
14
14
|
from datetime import datetime, date, timedelta
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
|
-
NEXO_DB = Path.home() / "
|
|
17
|
+
NEXO_DB = Path.home() / ".nexo" / "nexo-mcp" / "nexo.db"
|
|
18
18
|
CORTEX_DIR = Path(__file__).parent
|
|
19
|
-
CLAUDE_DIR = Path.home() / "
|
|
19
|
+
CLAUDE_DIR = Path.home() / ".nexo"
|
|
20
20
|
SANDBOX_DIR = CLAUDE_DIR / "sandbox" / "workspace"
|
|
21
21
|
SNAPSHOTS_DIR = CLAUDE_DIR / "snapshots"
|
|
22
22
|
OBJECTIVE_FILE = CORTEX_DIR / "evolution-objective.json"
|
|
@@ -162,97 +162,75 @@ def dry_run_restore_test() -> bool:
|
|
|
162
162
|
|
|
163
163
|
|
|
164
164
|
def build_evolution_prompt(week_data: dict, objective: dict) -> str:
|
|
165
|
-
"""Build
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
prompt
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if top_areas:
|
|
235
|
-
prompt += "- Top problem areas: " + ", ".join(f"{r[0]}({r[1]})" for r in top_areas) + "\n"
|
|
236
|
-
if most_ignored:
|
|
237
|
-
prompt += "- Most ignored learnings (3+ repeats): " + ", ".join(f"#{r[0]}({r[1]}x)" for r in most_ignored) + "\n"
|
|
238
|
-
prompt += "- Propose more aggressive rules for areas with high repetition rate.\n"
|
|
239
|
-
except Exception:
|
|
240
|
-
pass
|
|
241
|
-
|
|
242
|
-
# Infrastructure inventory — so Opus knows what exists before proposing changes
|
|
243
|
-
inventory_script = Path.home() / "claude" / "scripts" / "nexo-infra-inventory.sh"
|
|
244
|
-
if inventory_script.exists():
|
|
245
|
-
try:
|
|
246
|
-
result = subprocess.run(
|
|
247
|
-
["bash", str(inventory_script)],
|
|
248
|
-
capture_output=True, text=True, timeout=10
|
|
249
|
-
)
|
|
250
|
-
if result.stdout.strip():
|
|
251
|
-
prompt += f"\n### Infrastructure Inventory (hooks, scripts, memory, crons)\n"
|
|
252
|
-
prompt += "Before proposing any change, check this inventory to avoid duplicating existing infrastructure.\n"
|
|
253
|
-
prompt += f"```json\n{result.stdout.strip()}\n```\n"
|
|
254
|
-
except Exception:
|
|
255
|
-
pass
|
|
165
|
+
"""Build a SHORT prompt — CLI investigates on its own using tools."""
|
|
166
|
+
|
|
167
|
+
# Summary stats only — CLI will dig deeper with tools
|
|
168
|
+
stats = {
|
|
169
|
+
"learnings_this_week": len(week_data.get("learnings", [])),
|
|
170
|
+
"decisions_this_week": len(week_data.get("decisions", [])),
|
|
171
|
+
"changes_this_week": len(week_data.get("changes", [])),
|
|
172
|
+
"diaries_this_week": len(week_data.get("diaries", [])),
|
|
173
|
+
"evolution_history": len(week_data.get("evolution_history", [])),
|
|
174
|
+
"current_scores": {dim: m["score"] for dim, m in week_data.get("current_metrics", {}).items()},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
mode = objective.get("evolution_mode", "auto")
|
|
178
|
+
total = objective.get("total_evolutions", 0)
|
|
179
|
+
max_auto = max_auto_changes(total)
|
|
180
|
+
|
|
181
|
+
prompt = f"""You are NEXO Evolution — the weekly self-improvement cycle.
|
|
182
|
+
|
|
183
|
+
YOUR JOB: Analyze the past week and propose concrete improvements to NEXO's codebase.
|
|
184
|
+
|
|
185
|
+
WEEK SUMMARY:
|
|
186
|
+
- {stats['learnings_this_week']} new learnings
|
|
187
|
+
- {stats['decisions_this_week']} decisions made
|
|
188
|
+
- {stats['changes_this_week']} code changes deployed
|
|
189
|
+
- {stats['diaries_this_week']} session diaries
|
|
190
|
+
- {stats['evolution_history']} past evolution proposals
|
|
191
|
+
- Current scores: {json.dumps(stats['current_scores'])}
|
|
192
|
+
|
|
193
|
+
MODE: {mode} ({"proposals only, owner reviews" if mode == "review" else f"max {max_auto} auto-applied changes"})
|
|
194
|
+
CYCLE: #{total + 1}
|
|
195
|
+
|
|
196
|
+
INVESTIGATE using these tools:
|
|
197
|
+
1. Bash: sqlite3 {NEXO_DB} "SELECT category, title FROM learnings WHERE created_at > {time.time() - 7*86400} ORDER BY created_at DESC LIMIT 30"
|
|
198
|
+
2. Bash: sqlite3 {NEXO_DB} "SELECT area, COUNT(*) as cnt FROM error_repetitions GROUP BY area ORDER BY cnt DESC LIMIT 10"
|
|
199
|
+
3. Read ~/.nexo/coordination/daily-synthesis.md — today's context
|
|
200
|
+
4. Read ~/.nexo/coordination/postmortem-daily.md — self-critique patterns
|
|
201
|
+
5. Read ~/.nexo/logs/self-audit-summary.json — system health
|
|
202
|
+
6. Glob ~/.nexo/scripts/*.py — existing scripts
|
|
203
|
+
7. Glob ~/.nexo/nexo-mcp/plugins/*.py — existing plugins
|
|
204
|
+
|
|
205
|
+
LOOK FOR:
|
|
206
|
+
- Repeated errors that guard isn't preventing
|
|
207
|
+
- Scripts or processes that are failing or underperforming
|
|
208
|
+
- Missing functionality that session diaries keep asking for
|
|
209
|
+
- Redundant code or config that could be simplified
|
|
210
|
+
- Patterns in self-critique that suggest systemic issues
|
|
211
|
+
|
|
212
|
+
SAFETY:
|
|
213
|
+
- Safe zones for auto changes: ~/.nexo/scripts/, ~/.nexo/nexo-mcp/plugins/, ~/.nexo/cortex/
|
|
214
|
+
- IMMUTABLE files (never touch): db.py, server.py, plugin_loader.py, cognitive.py, CLAUDE.md
|
|
215
|
+
- Every change needs: what file, what to change, why, risk, how to verify
|
|
216
|
+
|
|
217
|
+
OUTPUT FORMAT (JSON):
|
|
218
|
+
{{
|
|
219
|
+
"analysis": "one paragraph summary of what you found",
|
|
220
|
+
"patterns": [{{"type": "...", "description": "...", "frequency": "..."}}],
|
|
221
|
+
"proposals": [
|
|
222
|
+
{{
|
|
223
|
+
"classification": "auto" or "propose",
|
|
224
|
+
"dimension": "reliability|proactivity|efficiency|safety|learning",
|
|
225
|
+
"action": "what to do",
|
|
226
|
+
"reasoning": "why",
|
|
227
|
+
"scope": "local",
|
|
228
|
+
"changes": [{{"file": "path", "operation": "create|replace|append", "search": "text to find", "content": "new text"}}]
|
|
229
|
+
}}
|
|
230
|
+
]
|
|
231
|
+
}}
|
|
232
|
+
|
|
233
|
+
Max 3 proposals. Quality over quantity. If nothing needs improving, say so."""
|
|
256
234
|
|
|
257
235
|
return prompt
|
|
258
236
|
|
|
Binary file
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Write session start timestamp for session-scoped tool counting
|
|
4
|
+
date +%s > "${NEXO_HOME:-$HOME/.nexo}/operations/.session-start-ts"
|
|
2
5
|
# NEXO SessionStart hook — generates a comprehensive briefing.
|
|
3
6
|
# Reads SQLite directly for reminders, followups, active sessions.
|
|
4
7
|
# Caches output for 1 hour to avoid regenerating on rapid successive sessions.
|
|
@@ -61,7 +64,7 @@ try:
|
|
|
61
64
|
try:
|
|
62
65
|
reminders_rows = [dict(r) for r in db.execute(
|
|
63
66
|
'SELECT id, date, description, status, category FROM reminders '
|
|
64
|
-
'WHERE status NOT LIKE \"%
|
|
67
|
+
'WHERE status NOT LIKE \"%COMPLET%\" AND status NOT LIKE \"%DELET%\" '
|
|
65
68
|
'AND status NOT LIKE \"%COMPLETED%\" AND status NOT LIKE \"%DELETED%\"'
|
|
66
69
|
).fetchall()]
|
|
67
70
|
except Exception:
|
|
@@ -70,7 +73,7 @@ try:
|
|
|
70
73
|
try:
|
|
71
74
|
followups_rows = [dict(r) for r in db.execute(
|
|
72
75
|
'SELECT id, date, description, status FROM followups '
|
|
73
|
-
'WHERE status NOT LIKE \"%
|
|
76
|
+
'WHERE status NOT LIKE \"%COMPLET%\" AND status NOT LIKE \"%COMPLETED%\"'
|
|
74
77
|
).fetchall()]
|
|
75
78
|
except Exception:
|
|
76
79
|
pass
|