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.
Files changed (90) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  4. package/src/__pycache__/cognitive.cpython-314.pyc +0 -0
  5. package/src/__pycache__/db.cpython-314.pyc +0 -0
  6. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  7. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  8. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  9. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  10. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  11. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  12. package/src/__pycache__/server.cpython-314.pyc +0 -0
  13. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  14. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  15. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  16. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  17. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  18. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  19. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  20. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  21. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  22. package/src/cognitive.py +63 -14
  23. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  24. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  25. package/src/dashboard/app.py +1 -1
  26. package/src/db.py +29 -13
  27. package/src/evolution_cycle.py +72 -94
  28. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  29. package/src/hooks/session-start.sh +5 -2
  30. package/src/hooks/session-stop.sh +79 -133
  31. package/src/knowledge_graph.py +3 -3
  32. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  33. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  34. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  35. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  36. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  37. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  38. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  39. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  40. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  41. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  42. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  43. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  44. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  45. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  46. package/src/plugins/artifact_registry.py +450 -0
  47. package/src/plugins/cognitive_memory.py +9 -9
  48. package/src/plugins/episodic_memory.py +8 -8
  49. package/src/plugins/evolution.py +4 -4
  50. package/src/plugins/guard.py +26 -2
  51. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  52. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  53. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  54. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  55. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  56. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  57. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  58. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  59. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  60. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  61. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  62. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  63. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  64. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  65. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  66. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  67. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  68. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  69. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  70. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  71. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  72. package/src/scripts/nexo-brain-activation.sh +140 -0
  73. package/src/scripts/nexo-evolution-run.py +1 -1
  74. package/src/scripts/nexo-followup-hygiene.py +107 -0
  75. package/src/scripts/nexo-postmortem-consolidator.py +25 -25
  76. package/src/scripts/nexo-pre-commit.py +118 -0
  77. package/src/scripts/nexo-proactive-dashboard.py +342 -0
  78. package/src/scripts/nexo-runtime-preflight.py +270 -0
  79. package/src/scripts/nexo-send-email.py +25 -0
  80. package/src/scripts/nexo-send-reply.py +176 -0
  81. package/src/scripts/nexo-snapshot-restore.sh +34 -0
  82. package/src/scripts/nexo-watchdog-smoke.py +114 -0
  83. package/src/server.py +5 -5
  84. package/src/tools_coordination.py +3 -3
  85. package/src/tools_learnings.py +1 -1
  86. package/src/tools_menu.py +31 -49
  87. package/src/tools_reminders_crud.py +1 -1
  88. package/src/tools_sessions.py +12 -12
  89. package/src/rules/__init__ 2.py +0 -0
  90. 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 "Start NEXO session. Run nexo_startup, load context, greet the user."
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.0",
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
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", "wazion", "project_a", "ecommerce", "whatsapp", "chrome", "firefox",
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 (cosine > 0.8 with opposite sentiment)."""
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
- contradictions.append({
2139
- "ltm_id": row["id"],
2140
- "content": row["content"][:200],
2141
- "similarity": round(score, 3),
2142
- "strength": row["strength"],
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: Conciso. Menos contexto, más acción directa."
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)
@@ -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:wazion", "file:path")
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 'PENDIENTE',
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 'PENDIENTE',
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 = 'PENDIENTE', category: str = 'general') -> dict:
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"COMPLETADO {today}")
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 'COMPLETADO%' ORDER BY updated_at DESC"
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 'COMPLETADO%' "
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 'COMPLETADO%' "
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 = 'PENDIENTE',
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"COMPLETADO {today}"}
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 'COMPLETADO%' ORDER BY updated_at DESC"
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 'COMPLETADO%' "
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 'COMPLETADO%' "
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. 'wazion', 'my-store')
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)
@@ -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 owner to review).
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() / "claude" / "nexo-mcp" / "nexo.db"
17
+ NEXO_DB = Path.home() / ".nexo" / "nexo-mcp" / "nexo.db"
18
18
  CORTEX_DIR = Path(__file__).parent
19
- CLAUDE_DIR = Path.home() / "claude"
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 the prompt for the Opus Evolution cycle."""
166
- if PROMPT_FILE.exists():
167
- template = PROMPT_FILE.read_text()
168
- else:
169
- template = "You are NEXO Evolution. Analyze the data and propose improvements."
170
-
171
- prompt = template + "\n\n## WEEKLY DATA\n\n"
172
- prompt += f"### Learnings ({len(week_data.get('learnings', []))} this week)\n"
173
- for l in week_data.get("learnings", [])[:30]:
174
- prompt += f"- [{l['category']}] {l['title']}: {str(l['content'])[:150]}\n"
175
-
176
- prompt += f"\n### Decisions ({len(week_data.get('decisions', []))} this week)\n"
177
- for d in week_data.get("decisions", [])[:15]:
178
- outcome = f" → {str(d['outcome'])[:80]}" if d.get("outcome") else " → no outcome yet"
179
- prompt += f"- [{d['domain']}] {str(d['decision'])[:150]}{outcome}\n"
180
-
181
- prompt += f"\n### Changes ({len(week_data.get('changes', []))} this week)\n"
182
- for c in week_data.get("changes", [])[:20]:
183
- prompt += f"- {str(c['files'])[:60]}: {str(c['what_changed'])[:100]}\n"
184
-
185
- prompt += f"\n### Session Diaries ({len(week_data.get('diaries', []))} this week)\n"
186
- for s in week_data.get("diaries", [])[:10]:
187
- prompt += f"- [{s.get('domain','')}] {str(s['summary'])[:150]}\n"
188
- if s.get("user_signals"):
189
- prompt += f" the owner: {str(s['user_signals'])[:100]}\n"
190
-
191
- prompt += "\n### Current Dimension Scores\n"
192
- for dim, m in week_data.get("current_metrics", {}).items():
193
- prompt += f"- {dim}: {m['score']}% (delta: {m.get('delta', 0)})\n"
194
-
195
- prompt += f"\n### Evolution History ({len(week_data.get('evolution_history', []))} entries)\n"
196
- for h in week_data.get("evolution_history", [])[:10]:
197
- prompt += f"- #{h['id']} [{h['status']}] {str(h['proposal'])[:100]}\n"
198
-
199
- prompt += f"\n### Objective\n{json.dumps(objective, indent=2)}\n"
200
-
201
- # Guard statserror prevention effectiveness
202
- try:
203
- guard_conn = sqlite3.connect(str(NEXO_DB), timeout=10)
204
- cutoff_7d = (date.today() - timedelta(days=7)).isoformat()
205
- cutoff_epoch_7d = time.time() - 7 * 86400
206
-
207
- total_reps = guard_conn.execute(
208
- "SELECT COUNT(*) FROM error_repetitions WHERE created_at > ?", (cutoff_7d,)
209
- ).fetchone()[0]
210
- new_learnings_7d = guard_conn.execute(
211
- "SELECT COUNT(*) FROM learnings WHERE created_at > ?", (cutoff_epoch_7d,)
212
- ).fetchone()[0]
213
- rep_rate = round(total_reps / new_learnings_7d, 2) if new_learnings_7d > 0 else 0.0
214
- guard_checks = guard_conn.execute(
215
- "SELECT COUNT(*) FROM guard_checks WHERE created_at > ?", (cutoff_7d,)
216
- ).fetchone()[0]
217
-
218
- top_areas = guard_conn.execute(
219
- "SELECT area, COUNT(*) as cnt FROM error_repetitions WHERE created_at > ? GROUP BY area ORDER BY cnt DESC LIMIT 5",
220
- (cutoff_7d,)
221
- ).fetchall()
222
-
223
- most_ignored = guard_conn.execute(
224
- "SELECT original_learning_id, COUNT(*) as cnt FROM error_repetitions "
225
- "GROUP BY original_learning_id HAVING cnt >= 3 ORDER BY cnt DESC LIMIT 5"
226
- ).fetchall()
227
-
228
- guard_conn.close()
229
-
230
- prompt += "\n### Guard Stats (Error Prevention)\n"
231
- prompt += f"- Repetition rate: {rep_rate:.0%} (target: <15%)\n"
232
- prompt += f"- Guard checks this week: {guard_checks} (target: >5/session)\n"
233
- prompt += f"- New learnings: {new_learnings_7d}, Repetitions: {total_reps}\n"
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.jsonsystem 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
 
@@ -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 \"%COMPLETADO%\" AND status NOT LIKE \"%ELIMINADO%\" '
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 \"%COMPLETADO%\" AND status NOT LIKE \"%COMPLETED%\"'
76
+ 'WHERE status NOT LIKE \"%COMPLET%\" AND status NOT LIKE \"%COMPLETED%\"'
74
77
  ).fetchall()]
75
78
  except Exception:
76
79
  pass