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
|
@@ -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)
|