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
@@ -0,0 +1,342 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NEXO Proactive Dashboard — Surfaces issues and opportunities without the user asking.
4
+
5
+ Scans: followups vencidos, reminders olvidados, learnings sin resolver,
6
+ inactive systems, user patterns, and more.
7
+
8
+ Usage:
9
+ python3 nexo-proactive-dashboard.py # Full scan, text output
10
+ python3 nexo-proactive-dashboard.py --json # JSON output for programmatic use
11
+ python3 nexo-proactive-dashboard.py --brief # One-liner alerts only
12
+ """
13
+
14
+ import json
15
+ import os
16
+ import sqlite3
17
+ import subprocess
18
+ import sys
19
+ from datetime import datetime, timedelta
20
+ from pathlib import Path
21
+
22
+ NEXO_DB = Path.home() / ".nexo" / "nexo-mcp" / "nexo.db"
23
+
24
+
25
+ def get_db():
26
+ conn = sqlite3.connect(str(NEXO_DB), timeout=10)
27
+ conn.row_factory = sqlite3.Row
28
+ return conn
29
+
30
+
31
+ def check_overdue_followups() -> list[dict]:
32
+ """Find followups that are overdue and not completed."""
33
+ conn = get_db()
34
+ now_epoch = datetime.now().timestamp()
35
+ rows = conn.execute("""
36
+ SELECT id, description, date, created_at, reasoning
37
+ FROM followups
38
+ WHERE status NOT LIKE 'COMPLETED%'
39
+ AND date IS NOT NULL AND date != ''
40
+ ORDER BY date ASC
41
+ """).fetchall()
42
+ conn.close()
43
+ alerts = []
44
+ for r in rows:
45
+ due_str = r["date"]
46
+ try:
47
+ due = datetime.fromisoformat(due_str) if due_str else None
48
+ if due and due < datetime.now():
49
+ days_overdue = (datetime.now() - due).days
50
+ alerts.append({
51
+ "type": "overdue_followup",
52
+ "severity": "high" if days_overdue > 3 else "medium",
53
+ "title": f"Followup vencido hace {days_overdue}d: {r['description'][:80]}",
54
+ "id": r["id"],
55
+ "days_overdue": days_overdue,
56
+ })
57
+ except (ValueError, TypeError):
58
+ pass
59
+ return alerts
60
+
61
+
62
+ def check_overdue_reminders() -> list[dict]:
63
+ """Find reminders that are overdue."""
64
+ conn = get_db()
65
+ rows = conn.execute("""
66
+ SELECT id, description, date, status
67
+ FROM reminders
68
+ WHERE status NOT IN ('COMPLETED', 'CANCELADO')
69
+ AND date IS NOT NULL AND date != ''
70
+ ORDER BY date ASC
71
+ """).fetchall()
72
+ conn.close()
73
+ alerts = []
74
+ for r in rows:
75
+ due_str = r["date"]
76
+ try:
77
+ due = datetime.fromisoformat(due_str) if due_str else None
78
+ if due and due < datetime.now():
79
+ days_overdue = (datetime.now() - due).days
80
+ alerts.append({
81
+ "type": "overdue_reminder",
82
+ "severity": "high" if days_overdue > 7 else "medium",
83
+ "title": f"Reminder vencido hace {days_overdue}d: {r['description'][:80]}",
84
+ "id": r["id"],
85
+ "days_overdue": days_overdue,
86
+ })
87
+ except (ValueError, TypeError):
88
+ pass
89
+ return alerts
90
+
91
+
92
+ def check_stale_ideas() -> list[dict]:
93
+ """Find reminders/ideas without due dates that have been sitting for too long."""
94
+ conn = get_db()
95
+ rows = conn.execute("""
96
+ SELECT id, description, created_at
97
+ FROM reminders
98
+ WHERE status NOT IN ('COMPLETED', 'CANCELADO')
99
+ AND (date IS NULL OR date = '')
100
+ ORDER BY created_at ASC
101
+ """).fetchall()
102
+ conn.close()
103
+ alerts = []
104
+ stale_count = 0
105
+ for r in rows:
106
+ try:
107
+ # created_at is epoch float
108
+ created = datetime.fromtimestamp(r["created_at"])
109
+ age_days = (datetime.now() - created).days
110
+ except (ValueError, TypeError, OSError):
111
+ age_days = 0
112
+ if age_days > 14:
113
+ stale_count += 1
114
+
115
+ if stale_count > 10:
116
+ alerts.append({
117
+ "type": "stale_ideas",
118
+ "severity": "low",
119
+ "title": f"{stale_count} ideas/reminders without date are >14 days old. Review or archive.",
120
+ "count": stale_count,
121
+ })
122
+ return alerts
123
+
124
+
125
+ def check_session_gaps() -> list[dict]:
126
+ """Detect if NEXO hasn't been active for unusual periods."""
127
+ conn = get_db()
128
+ row = conn.execute("""
129
+ SELECT MAX(created_at) as last_diary FROM session_diary
130
+ """).fetchone()
131
+ conn.close()
132
+ alerts = []
133
+ if row and row["last_diary"]:
134
+ try:
135
+ last = datetime.fromisoformat(row["last_diary"])
136
+ gap_hours = (datetime.now() - last).total_seconds() / 3600
137
+ if gap_hours > 48:
138
+ alerts.append({
139
+ "type": "session_gap",
140
+ "severity": "low",
141
+ "title": f"No sessions registered in {gap_hours:.0f}h ({gap_hours/24:.1f} days)",
142
+ "gap_hours": gap_hours,
143
+ })
144
+ except (ValueError, TypeError):
145
+ pass
146
+ return alerts
147
+
148
+
149
+ def check_evolution_status() -> list[dict]:
150
+ """Check if evolution system is healthy."""
151
+ alerts = []
152
+ obj_file = Path.home() / ".nexo" / "cortex" / "evolution-objective.json"
153
+ if obj_file.exists():
154
+ obj = json.loads(obj_file.read_text())
155
+ if not obj.get("evolution_enabled", True):
156
+ alerts.append({
157
+ "type": "evolution_disabled",
158
+ "severity": "high",
159
+ "title": f"Evolution DESACTIVADO: {obj.get('disabled_reason', 'desconocido')}",
160
+ })
161
+ if obj.get("consecutive_failures", 0) > 0:
162
+ alerts.append({
163
+ "type": "evolution_failures",
164
+ "severity": "medium",
165
+ "title": f"Evolution: {obj['consecutive_failures']} fallos consecutivos",
166
+ })
167
+
168
+ # Check dimension regression
169
+ for dim, data in obj.get("dimensions", {}).items():
170
+ current = data.get("current", 0)
171
+ if current < 30:
172
+ alerts.append({
173
+ "type": "dimension_low",
174
+ "severity": "medium",
175
+ "title": f"Dimension '{dim}' baja: {current}%",
176
+ "dimension": dim,
177
+ "score": current,
178
+ })
179
+ return alerts
180
+
181
+
182
+ def check_pending_proposals() -> list[dict]:
183
+ """Check for evolution proposals awaiting the user's review."""
184
+ conn = get_db()
185
+ rows = conn.execute("""
186
+ SELECT id, dimension, proposal, created_at
187
+ FROM evolution_log
188
+ WHERE status = 'proposed' AND classification = 'propose'
189
+ ORDER BY created_at DESC
190
+ """).fetchall()
191
+ conn.close()
192
+ if rows:
193
+ return [{
194
+ "type": "pending_proposals",
195
+ "severity": "low",
196
+ "title": f"{len(rows)} pending evolution proposals awaiting review",
197
+ "count": len(rows),
198
+ "proposals": [{"id": r["id"], "dim": r["dimension"], "text": r["proposal"][:80]} for r in rows],
199
+ }]
200
+ return []
201
+
202
+
203
+ def check_recurring_errors() -> list[dict]:
204
+ """Detect learnings that keep appearing (same issue reported multiple times)."""
205
+ conn = get_db()
206
+ rows = conn.execute("""
207
+ SELECT category, COUNT(*) as cnt
208
+ FROM learnings
209
+ WHERE created_at > datetime('now', '-7 days')
210
+ GROUP BY category
211
+ HAVING cnt >= 5
212
+ ORDER BY cnt DESC
213
+ """).fetchall()
214
+ conn.close()
215
+ alerts = []
216
+ for r in rows:
217
+ alerts.append({
218
+ "type": "recurring_errors",
219
+ "severity": "medium",
220
+ "title": f"Categoria '{r['category']}' tiene {r['cnt']} learnings esta semana — posible problema sistémico",
221
+ "category": r["category"],
222
+ "count": r["cnt"],
223
+ })
224
+ return alerts
225
+
226
+
227
+ def check_cron_health() -> list[dict]:
228
+ """Check if critical cron jobs are running."""
229
+ alerts = []
230
+
231
+ # Check backup cron
232
+ backup_dir = Path.home() / ".nexo" / "nexo-mcp" / "backups"
233
+ if backup_dir.exists():
234
+ backups = sorted(backup_dir.glob("nexo-*.db"), key=lambda p: p.stat().st_mtime, reverse=True)
235
+ if backups:
236
+ last_backup_age = (datetime.now().timestamp() - backups[0].stat().st_mtime) / 3600
237
+ if last_backup_age > 4:
238
+ alerts.append({
239
+ "type": "backup_stale",
240
+ "severity": "high",
241
+ "title": f"Ultimo backup de nexo.db hace {last_backup_age:.1f}h (deberia ser cada 1h)",
242
+ })
243
+
244
+ # Check immune system
245
+ immune_status = Path.home() / ".nexo" / "coordination" / "immune-status.json"
246
+ if immune_status.exists():
247
+ try:
248
+ status = json.loads(immune_status.read_text())
249
+ if status.get("status") == "degraded":
250
+ alerts.append({
251
+ "type": "immune_degraded",
252
+ "severity": "high",
253
+ "title": f"Sistema inmune degradado: {status.get('reason', '?')}",
254
+ })
255
+ except (json.JSONDecodeError, KeyError):
256
+ pass
257
+
258
+ return alerts
259
+
260
+
261
+ def run_all_checks() -> list[dict]:
262
+ """Run all proactive checks and return sorted alerts."""
263
+ all_alerts = []
264
+ checks = [
265
+ check_overdue_followups,
266
+ check_overdue_reminders,
267
+ check_stale_ideas,
268
+ check_session_gaps,
269
+ check_evolution_status,
270
+ check_pending_proposals,
271
+ check_recurring_errors,
272
+ check_cron_health,
273
+ ]
274
+
275
+ for check in checks:
276
+ try:
277
+ all_alerts.extend(check())
278
+ except Exception as e:
279
+ all_alerts.append({
280
+ "type": "check_error",
281
+ "severity": "low",
282
+ "title": f"Check {check.__name__} failed: {e}",
283
+ })
284
+
285
+ # Sort by severity
286
+ severity_order = {"high": 0, "medium": 1, "low": 2}
287
+ all_alerts.sort(key=lambda a: severity_order.get(a.get("severity", "low"), 3))
288
+
289
+ return all_alerts
290
+
291
+
292
+ def format_text(alerts: list[dict]) -> str:
293
+ """Format alerts as readable text."""
294
+ if not alerts:
295
+ return "No hay alertas proactivas. Todo en orden."
296
+
297
+ severity_icons = {"high": "!!!", "medium": " ! ", "low": " . "}
298
+ lines = [f"NEXO Proactive Dashboard — {len(alerts)} alertas\n"]
299
+
300
+ current_severity = None
301
+ for a in alerts:
302
+ sev = a.get("severity", "low")
303
+ if sev != current_severity:
304
+ current_severity = sev
305
+ label = {"high": "URGENTE", "medium": "ATENCION", "low": "INFO"}.get(sev, sev)
306
+ lines.append(f"\n [{label}]")
307
+ icon = severity_icons.get(sev, " . ")
308
+ lines.append(f" {icon} {a['title']}")
309
+
310
+ return "\n".join(lines)
311
+
312
+
313
+ def format_brief(alerts: list[dict]) -> str:
314
+ """One-liner summary."""
315
+ high = sum(1 for a in alerts if a.get("severity") == "high")
316
+ med = sum(1 for a in alerts if a.get("severity") == "medium")
317
+ low = sum(1 for a in alerts if a.get("severity") == "low")
318
+ if not alerts:
319
+ return "Dashboard: limpio"
320
+ return f"Dashboard: {high} urgente, {med} atencion, {low} info"
321
+
322
+
323
+ def main():
324
+ output_json = "--json" in sys.argv
325
+ brief = "--brief" in sys.argv
326
+
327
+ alerts = run_all_checks()
328
+
329
+ if output_json:
330
+ print(json.dumps(alerts, indent=2, default=str))
331
+ elif brief:
332
+ print(format_brief(alerts))
333
+ else:
334
+ print(format_text(alerts))
335
+
336
+ # Exit code = number of high severity alerts
337
+ high_count = sum(1 for a in alerts if a.get("severity") == "high")
338
+ sys.exit(min(high_count, 125))
339
+
340
+
341
+ if __name__ == "__main__":
342
+ main()
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NEXO Runtime Preflight
4
+
5
+ Runs safe end-to-end smoke tests for Cortex and Evolution using a temporary
6
+ workspace and a copied SQLite database. No external API calls are performed.
7
+ Results are written to ~/.nexo/logs/runtime-preflight-summary.json.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib.util
13
+ import json
14
+ import shutil
15
+ import sqlite3
16
+ import sys
17
+ import tempfile
18
+ import traceback
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+
22
+ HOME = Path.home()
23
+ CLAUDE_DIR = HOME / ".nexo"
24
+ LOG_DIR = CLAUDE_DIR / "logs"
25
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
26
+ SUMMARY_FILE = LOG_DIR / "runtime-preflight-summary.json"
27
+ DB_FILE = CLAUDE_DIR / "nexo-mcp" / "nexo.db"
28
+ CORTEX_OBJECTIVE = CLAUDE_DIR / "cortex" / "evolution-objective.json"
29
+ CORTEX_PROMPT = CLAUDE_DIR / "cortex" / "evolution-prompt.md"
30
+
31
+
32
+ def _load_module(name: str, path: Path):
33
+ spec = importlib.util.spec_from_file_location(name, str(path))
34
+ module = importlib.util.module_from_spec(spec)
35
+ sys.modules[name] = module
36
+ assert spec.loader is not None
37
+ spec.loader.exec_module(module)
38
+ return module
39
+
40
+
41
+ def _write_summary(summary: dict):
42
+ SUMMARY_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False))
43
+
44
+
45
+ def _fake_cortex_response(model: str) -> str:
46
+ if "opus" in model:
47
+ payload = {
48
+ "analysis": "Smoke evolution run over temp workspace",
49
+ "proposals": [
50
+ {
51
+ "dimension": "self_improvement",
52
+ "classification": "propose",
53
+ "action": "Add a regression smoke before live cycles",
54
+ "reasoning": "Smoke run should stay side-effect free",
55
+ }
56
+ ],
57
+ "dimension_scores": {
58
+ "episodic_memory": 86,
59
+ "autonomy": 53,
60
+ "proactivity": 34,
61
+ "self_improvement": 30,
62
+ "agi": 6,
63
+ },
64
+ "score_evidence": {
65
+ "self_improvement": "Temp evolution smoke completed successfully",
66
+ },
67
+ }
68
+ else:
69
+ payload = {
70
+ "actions_taken": ["preflight perception completed"],
71
+ "signals_detected": 1,
72
+ "pending_questions": [],
73
+ "execute": [],
74
+ "next_interval_suggestion": 600,
75
+ "reflection_done": False,
76
+ "dmn_done": False,
77
+ "briefing_update": {
78
+ "actions_taken": ["perception smoke ok"],
79
+ "signals_active": [{"summary": "Smoke signal", "urgency": "INFO", "score": 10}],
80
+ "recommendations": ["keep runtime checks green"],
81
+ "pending_questions_unanswered": [],
82
+ "dmn_summary": "",
83
+ },
84
+ "working_memory": {
85
+ "current_threads": ["runtime preflight"],
86
+ "attention_focus": "runtime integrity",
87
+ "last_reasoning": "API mocked successfully",
88
+ "watching": ["state writes", "briefing writes"],
89
+ "momentum": "stable",
90
+ },
91
+ "error": None,
92
+ }
93
+ return json.dumps(payload, ensure_ascii=False)
94
+
95
+
96
+ def _fake_runner_response() -> tuple[str, dict]:
97
+ payload = {
98
+ "analysis": "Standalone evolution smoke over temp DB",
99
+ "patterns": [
100
+ {"type": "smoke", "description": "runner executed over temp workspace", "frequency": "once"}
101
+ ],
102
+ "proposals": [
103
+ {
104
+ "dimension": "self_improvement",
105
+ "classification": "propose",
106
+ "action": "Keep standalone evolution covered by preflight",
107
+ "reasoning": "Regression prevention",
108
+ }
109
+ ],
110
+ "dimension_scores": {
111
+ "episodic_memory": 87,
112
+ "autonomy": 54,
113
+ "proactivity": 35,
114
+ "self_improvement": 31,
115
+ "agi": 6,
116
+ },
117
+ "score_evidence": {
118
+ "self_improvement": "standalone runner smoke ok",
119
+ },
120
+ }
121
+ usage = {"input_tokens": 1234, "output_tokens": 432}
122
+ return json.dumps(payload, ensure_ascii=False), usage
123
+
124
+
125
+ def main() -> int:
126
+ started = datetime.now().isoformat()
127
+ summary = {
128
+ "timestamp": started,
129
+ "ok": False,
130
+ "checks": {},
131
+ "errors": [],
132
+ }
133
+
134
+ if not DB_FILE.exists():
135
+ summary["errors"].append("nexo.db missing")
136
+ _write_summary(summary)
137
+ return 1
138
+
139
+ preflight_root = HOME / ".codex" / "memories"
140
+ preflight_root.mkdir(parents=True, exist_ok=True)
141
+ temp_root = Path(tempfile.mkdtemp(prefix="nexo-runtime-preflight-", dir=str(preflight_root)))
142
+ try:
143
+ temp_db = temp_root / "nexo.db"
144
+ shutil.copy2(DB_FILE, temp_db)
145
+
146
+ temp_cortex_dir = temp_root / "cortex"
147
+ temp_logs_dir = temp_root / "logs"
148
+ temp_coord_dir = temp_root / "coordination"
149
+ temp_daily_dir = temp_root / "daily_summaries"
150
+ temp_dmn_dir = temp_root / "dmn_insights"
151
+ temp_snapshots_dir = temp_root / "snapshots"
152
+ temp_sandbox_dir = temp_root / "sandbox" / "workspace"
153
+ temp_scripts_dir = temp_root / "scripts"
154
+ for directory in [
155
+ temp_cortex_dir, temp_logs_dir, temp_coord_dir, temp_daily_dir,
156
+ temp_dmn_dir, temp_snapshots_dir, temp_sandbox_dir, temp_scripts_dir,
157
+ ]:
158
+ directory.mkdir(parents=True, exist_ok=True)
159
+
160
+ temp_objective = temp_cortex_dir / "evolution-objective.json"
161
+ temp_prompt = temp_cortex_dir / "evolution-prompt.md"
162
+ shutil.copy2(CORTEX_OBJECTIVE, temp_objective)
163
+ if CORTEX_PROMPT.exists():
164
+ shutil.copy2(CORTEX_PROMPT, temp_prompt)
165
+ shutil.copy2(CLAUDE_DIR / "scripts" / "nexo-snapshot-restore.sh", temp_scripts_dir / "nexo-snapshot-restore.sh")
166
+
167
+ temp_api_key = temp_root / "anthropic-api-key.txt"
168
+ temp_api_key.write_text("smoke-test-key")
169
+
170
+ evolution_cycle = _load_module("evolution_cycle", CLAUDE_DIR / "cortex" / "evolution_cycle.py")
171
+ evolution_cycle.NEXO_DB = temp_db
172
+ evolution_cycle.CLAUDE_DIR = temp_root
173
+ evolution_cycle.CORTEX_DIR = temp_cortex_dir
174
+ evolution_cycle.SANDBOX_DIR = temp_sandbox_dir
175
+ evolution_cycle.SNAPSHOTS_DIR = temp_snapshots_dir
176
+ evolution_cycle.OBJECTIVE_FILE = temp_objective
177
+ evolution_cycle.PROMPT_FILE = temp_prompt
178
+ evolution_cycle.RESTORE_LOG = temp_logs_dir / "snapshot-restores.log"
179
+
180
+ week_data = evolution_cycle.get_week_data(str(temp_db))
181
+ prompt = evolution_cycle.build_evolution_prompt(week_data, evolution_cycle.load_objective())
182
+ restore_ok = evolution_cycle.dry_run_restore_test()
183
+ if not restore_ok:
184
+ raise RuntimeError("dry_run_restore_test failed")
185
+ summary["checks"]["evolution_cycle"] = {
186
+ "learnings": len(week_data.get("learnings", [])),
187
+ "decisions": len(week_data.get("decisions", [])),
188
+ "prompt_chars": len(prompt),
189
+ "restore_ok": restore_ok,
190
+ }
191
+
192
+ cortex = _load_module("cortex_wrapper", CLAUDE_DIR / "cortex" / "cortex-wrapper.py")
193
+ cortex.NEXO_DB = temp_db
194
+ cortex.DRY_RUN = True
195
+ cortex.BRIEFING_FILE = temp_cortex_dir / "briefing.json"
196
+ cortex.HEALTH_FILE = temp_cortex_dir / "health.json"
197
+ cortex.STATE_FILE = temp_cortex_dir / "state.json"
198
+ cortex.STATE_TMP = temp_cortex_dir / "state.tmp"
199
+ cortex.LOG_DIR = temp_logs_dir
200
+ cortex.COORD_DIR = temp_coord_dir
201
+ cortex.SIGNALS_FILE = temp_coord_dir / "pending-signals.json"
202
+ cortex.DAILY_SUMMARIES_DIR = temp_daily_dir
203
+ cortex.DMN_INSIGHTS_DIR = temp_dmn_dir
204
+ cortex.FAILURE_COUNT_FILE = temp_cortex_dir / ".failure-count"
205
+ cortex.PID_FILE = temp_cortex_dir / "cortex.pid"
206
+ cortex.API_KEY_FILE = temp_api_key
207
+ cortex.wa_notify = lambda *args, **kwargs: None
208
+ cortex.poll_wa_inbox = lambda state: None
209
+ cortex._check_health_endpoints = lambda: []
210
+ cortex._call_anthropic = lambda prompt, model=None, max_tokens=4096: _fake_cortex_response(model or "")
211
+
212
+ state = cortex.load_state()
213
+ state = cortex.run_perception_cycle(state)
214
+ if not cortex.BRIEFING_FILE.exists() or not cortex.HEALTH_FILE.exists():
215
+ raise RuntimeError("cortex perception did not write briefing/health")
216
+
217
+ before_logs = sqlite3.connect(str(temp_db)).execute("SELECT COUNT(*) FROM evolution_log").fetchone()[0]
218
+ state = cortex.run_evolution_cycle(state)
219
+ after_logs = sqlite3.connect(str(temp_db)).execute("SELECT COUNT(*) FROM evolution_log").fetchone()[0]
220
+ if after_logs <= before_logs:
221
+ raise RuntimeError("cortex evolution smoke did not log proposals")
222
+ summary["checks"]["cortex_wrapper"] = {
223
+ "state_status": state.get("status"),
224
+ "briefing_written": cortex.BRIEFING_FILE.exists(),
225
+ "health_written": cortex.HEALTH_FILE.exists(),
226
+ "evolution_logs_added": after_logs - before_logs,
227
+ }
228
+
229
+ runner = _load_module("nexo_evolution_run", CLAUDE_DIR / "scripts" / "nexo-evolution-run.py")
230
+ runner.CLAUDE_DIR = temp_root
231
+ runner.NEXO_DB = temp_db
232
+ runner.CORTEX_DIR = temp_cortex_dir
233
+ runner.OBJECTIVE_FILE = temp_objective
234
+ runner.LOG_DIR = temp_logs_dir
235
+ runner.LOG_FILE = temp_logs_dir / "evolution.log"
236
+ runner.SNAPSHOTS_DIR = temp_snapshots_dir
237
+ runner.SANDBOX_DIR = temp_sandbox_dir
238
+ runner.API_KEY_FILE = temp_api_key
239
+ runner.BUDGET_FILE = temp_logs_dir / "evolution-budget.json"
240
+ runner.call_anthropic = lambda prompt: _fake_runner_response()
241
+ runner.check_budget = lambda: True
242
+
243
+ before_runner_logs = sqlite3.connect(str(temp_db)).execute("SELECT COUNT(*) FROM evolution_log").fetchone()[0]
244
+ runner.run()
245
+ conn = sqlite3.connect(str(temp_db))
246
+ after_runner_logs = conn.execute("SELECT COUNT(*) FROM evolution_log").fetchone()[0]
247
+ latest_cycle = conn.execute("SELECT MAX(cycle_number) FROM evolution_log").fetchone()[0]
248
+ conn.close()
249
+ if after_runner_logs <= before_runner_logs:
250
+ raise RuntimeError("standalone evolution runner did not log proposals")
251
+ summary["checks"]["standalone_runner"] = {
252
+ "evolution_logs_added": after_runner_logs - before_runner_logs,
253
+ "latest_cycle": latest_cycle,
254
+ "budget_file_written": runner.BUDGET_FILE.exists(),
255
+ }
256
+
257
+ summary["ok"] = True
258
+ _write_summary(summary)
259
+ return 0
260
+ except Exception as exc:
261
+ summary["errors"].append(f"{type(exc).__name__}: {exc}")
262
+ summary["errors"].append(traceback.format_exc())
263
+ _write_summary(summary)
264
+ return 1
265
+ finally:
266
+ shutil.rmtree(temp_root, ignore_errors=True)
267
+
268
+
269
+ if __name__ == "__main__":
270
+ sys.exit(main())
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ """Quick email sender for NEXO progress updates."""
3
+ import smtplib, sys
4
+ from email.mime.text import MIMEText
5
+ from email.utils import formataddr
6
+
7
+ def send(subject, body, to="userp@gmail.com", cc="user.example.com@gmail.com"):
8
+ msg = MIMEText(body, 'plain', 'utf-8')
9
+ msg['From'] = formataddr(('NEXO', 'nexo@example.com'))
10
+ msg['To'] = to
11
+ msg['Cc'] = cc
12
+ msg['Subject'] = subject
13
+ smtp = smtplib.SMTP_SSL('cl105e.server.com', 465)
14
+ smtp.login(FROM_EMAIL, os.environ.get('NEXO_SMTP_PASSWORD', ''))
15
+ recipients = [to]
16
+ if cc:
17
+ recipients.append(cc)
18
+ smtp.send_message(msg)
19
+ smtp.quit()
20
+ print(f"OK — sent to {to}")
21
+
22
+ if __name__ == "__main__":
23
+ subject = sys.argv[1] if len(sys.argv) > 1 else "NEXO Update"
24
+ body = sys.argv[2] if len(sys.argv) > 2 else "No body"
25
+ send(subject, body)