nexo-brain 5.3.19 → 5.3.21

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 (211) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/nexo-brain.js +52 -10
  3. package/package.json +1 -1
  4. package/src/auto_update.py +11 -8
  5. package/src/dashboard/static/favicon 2.svg +32 -0
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +40 -0
  8. package/src/dashboard/static/style 2.css +2458 -0
  9. package/src/dashboard/templates/adaptive 2.html +118 -0
  10. package/src/dashboard/templates/artifacts 2.html +133 -0
  11. package/src/dashboard/templates/backups 2.html +136 -0
  12. package/src/dashboard/templates/base 2.html +417 -0
  13. package/src/dashboard/templates/calendar 2.html +591 -0
  14. package/src/dashboard/templates/chat 2.html +356 -0
  15. package/src/dashboard/templates/claims 2.html +259 -0
  16. package/src/dashboard/templates/cortex 2.html +321 -0
  17. package/src/dashboard/templates/credentials 2.html +128 -0
  18. package/src/dashboard/templates/crons 2.html +370 -0
  19. package/src/dashboard/templates/dashboard 2.html +494 -0
  20. package/src/dashboard/templates/dreams 2.html +252 -0
  21. package/src/dashboard/templates/email 2.html +160 -0
  22. package/src/dashboard/templates/evolution 2.html +189 -0
  23. package/src/dashboard/templates/feed 2.html +249 -0
  24. package/src/dashboard/templates/followup_health 2.html +170 -0
  25. package/src/dashboard/templates/graph 2.html +201 -0
  26. package/src/dashboard/templates/guard 2.html +259 -0
  27. package/src/dashboard/templates/inbox 2.html +251 -0
  28. package/src/dashboard/templates/memory 2.html +420 -0
  29. package/src/dashboard/templates/operations 2.html +608 -0
  30. package/src/dashboard/templates/plugins 2.html +185 -0
  31. package/src/dashboard/templates/protocol 2.html +199 -0
  32. package/src/dashboard/templates/rules 2.html +246 -0
  33. package/src/dashboard/templates/sentiment 2.html +247 -0
  34. package/src/dashboard/templates/sessions 2.html +218 -0
  35. package/src/dashboard/templates/skills 2.html +329 -0
  36. package/src/dashboard/templates/somatic 2.html +73 -0
  37. package/src/dashboard/templates/triggers 2.html +133 -0
  38. package/src/dashboard/templates/trust 2.html +360 -0
  39. package/src/db/__init__ 2.py +259 -0
  40. package/src/db/_core 2.py +437 -0
  41. package/src/db/_credentials 2.py +124 -0
  42. package/src/db/_episodic 2.py +762 -0
  43. package/src/db/_evolution 2.py +54 -0
  44. package/src/db/_fts 2.py +406 -0
  45. package/src/db/_goal_profiles 2.py +376 -0
  46. package/src/db/_hot_context 2.py +660 -0
  47. package/src/db/_outcomes 2.py +800 -0
  48. package/src/db/_personal_scripts 2.py +582 -0
  49. package/src/db/_sessions 2.py +330 -0
  50. package/src/db/_tasks 2.py +91 -0
  51. package/src/db/_watchers 2.py +173 -0
  52. package/src/doctor/formatters 2.py +52 -0
  53. package/src/doctor/models 2.py +69 -0
  54. package/src/doctor/planes 2.py +87 -0
  55. package/src/doctor/providers/__init__ 2.py +1 -0
  56. package/src/doctor/providers/deep 2.py +367 -0
  57. package/src/evolution_cycle 2.py +519 -0
  58. package/src/hooks/auto_capture 2.py +208 -0
  59. package/src/hooks/caffeinate-guard 2.sh +8 -0
  60. package/src/hooks/capture-session 2.sh +21 -0
  61. package/src/hooks/capture-tool-logs 2.sh +158 -0
  62. package/src/hooks/daily-briefing-check 2.sh +33 -0
  63. package/src/hooks/heartbeat-enforcement 2.py +90 -0
  64. package/src/hooks/heartbeat-posttool 2.sh +18 -0
  65. package/src/hooks/inbox-hook 2.sh +76 -0
  66. package/src/hooks/post-compact 2.sh +152 -0
  67. package/src/hooks/pre-compact 2.sh +169 -0
  68. package/src/hooks/protocol-guardrail 2.sh +10 -0
  69. package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
  70. package/src/hooks/session-stop 2.sh +52 -0
  71. package/src/kg_populate 2.py +292 -0
  72. package/src/maintenance 2.py +53 -0
  73. package/src/memory_backends 2.py +71 -0
  74. package/src/migrate_embeddings 2.py +124 -0
  75. package/src/nexo_sdk 2.py +103 -0
  76. package/src/observability 2.py +199 -0
  77. package/src/plugin_loader 2.py +217 -0
  78. package/src/plugins/__init__ 2.py +0 -0
  79. package/src/plugins/artifact_registry 2.py +450 -0
  80. package/src/plugins/backup 2.py +127 -0
  81. package/src/plugins/claims_tools 2.py +119 -0
  82. package/src/plugins/cognitive_memory 2.py +609 -0
  83. package/src/plugins/core_rules 2.py +252 -0
  84. package/src/plugins/cortex 2.py +1155 -0
  85. package/src/plugins/entities 2.py +67 -0
  86. package/src/plugins/episodic_memory 2.py +560 -0
  87. package/src/plugins/evolution 2.py +167 -0
  88. package/src/plugins/goal_engine 2.py +142 -0
  89. package/src/plugins/guard 2.py +862 -0
  90. package/src/plugins/impact 2.py +29 -0
  91. package/src/plugins/knowledge_graph_tools 2.py +137 -0
  92. package/src/plugins/media_memory_tools 2.py +98 -0
  93. package/src/plugins/memory_export 2.py +196 -0
  94. package/src/plugins/outcomes 2.py +130 -0
  95. package/src/plugins/personal_scripts 2.py +117 -0
  96. package/src/plugins/preferences 2.py +47 -0
  97. package/src/plugins/protocol 2.py +1449 -0
  98. package/src/plugins/simple_api 2.py +106 -0
  99. package/src/plugins/skills 2.py +341 -0
  100. package/src/plugins/state_watchers 2.py +79 -0
  101. package/src/plugins/update 2.py +986 -0
  102. package/src/plugins/user_state_tools 2.py +43 -0
  103. package/src/plugins/workflow 2.py +588 -0
  104. package/src/protocol_settings 2.py +59 -0
  105. package/src/public_contribution 2.py +466 -0
  106. package/src/public_evolution_queue 2.py +241 -0
  107. package/src/requirements 2.txt +14 -0
  108. package/src/retroactive_learnings 2.py +373 -0
  109. package/src/rules/__init__ 2.py +0 -0
  110. package/src/rules/core-rules 2.json +331 -0
  111. package/src/rules/migrate 2.py +207 -0
  112. package/src/runtime_power 2.py +874 -0
  113. package/src/script_registry 2.py +1559 -0
  114. package/src/scripts/check-context 2.py +272 -0
  115. package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
  116. package/src/scripts/deep-sleep/collect 2.py +928 -0
  117. package/src/scripts/deep-sleep/extract 2.py +330 -0
  118. package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
  119. package/src/scripts/deep-sleep/synthesize 2.py +312 -0
  120. package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
  121. package/src/scripts/nexo-agent-run 2.py +75 -0
  122. package/src/scripts/nexo-auto-update 2.py +6 -0
  123. package/src/scripts/nexo-backup 2.sh +25 -0
  124. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  125. package/src/scripts/nexo-catchup 2.py +300 -0
  126. package/src/scripts/nexo-cognitive-decay 2.py +257 -0
  127. package/src/scripts/nexo-cortex-cycle 2.py +293 -0
  128. package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
  129. package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
  130. package/src/scripts/nexo-dashboard 2.sh +29 -0
  131. package/src/scripts/nexo-deep-sleep 2.sh +86 -0
  132. package/src/scripts/nexo-evolution-run 2.py +1664 -0
  133. package/src/scripts/nexo-followup-hygiene 2.py +139 -0
  134. package/src/scripts/nexo-hook-record 2.py +42 -0
  135. package/src/scripts/nexo-immune 2.py +936 -0
  136. package/src/scripts/nexo-impact-scorer 2.py +117 -0
  137. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  138. package/src/scripts/nexo-install 2.py +6 -0
  139. package/src/scripts/nexo-learning-housekeep 2.py +401 -0
  140. package/src/scripts/nexo-learning-validator 2.py +266 -0
  141. package/src/scripts/nexo-migrate 2.py +260 -0
  142. package/src/scripts/nexo-outcome-checker 2.py +127 -0
  143. package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
  144. package/src/scripts/nexo-pre-commit 2.py +120 -0
  145. package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
  146. package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
  147. package/src/scripts/nexo-reflection 2.py +256 -0
  148. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  149. package/src/scripts/nexo-sleep 2.py +631 -0
  150. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  151. package/src/scripts/nexo-sync-clients 2.py +16 -0
  152. package/src/scripts/nexo-synthesis 2.py +475 -0
  153. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  154. package/src/scripts/nexo-update 2.sh +306 -0
  155. package/src/scripts/nexo-watchdog 2.sh +1207 -0
  156. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  157. package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
  158. package/src/server 2.py +1296 -0
  159. package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
  160. package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
  161. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
  162. package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
  163. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
  164. package/src/skills/run-release-final-audit/guide 2.md +16 -0
  165. package/src/skills/run-release-final-audit/script 2.py +259 -0
  166. package/src/skills/run-release-final-audit/skill 2.json +77 -0
  167. package/src/skills/run-runtime-doctor/guide 2.md +12 -0
  168. package/src/skills/run-runtime-doctor/script 2.py +21 -0
  169. package/src/skills/run-runtime-doctor/skill 2.json +25 -0
  170. package/src/skills_runtime 2.py +932 -0
  171. package/src/state_watchers_runtime 2.py +475 -0
  172. package/src/storage_router 2.py +32 -0
  173. package/src/system_catalog 2.py +786 -0
  174. package/src/tools_coordination 2.py +103 -0
  175. package/src/tools_credentials 2.py +68 -0
  176. package/src/tools_drive 2.py +487 -0
  177. package/src/tools_hot_context 2.py +163 -0
  178. package/src/tools_learnings 2.py +612 -0
  179. package/src/tools_menu 2.py +229 -0
  180. package/src/tools_reminders 2.py +88 -0
  181. package/src/tools_reminders_crud 2.py +363 -0
  182. package/src/tools_sessions 2.py +1054 -0
  183. package/src/tools_system_catalog 2.py +19 -0
  184. package/src/tools_task_history 2.py +57 -0
  185. package/src/tools_transcripts 2.py +98 -0
  186. package/src/transcript_utils 2.py +412 -0
  187. package/src/user_context 2.py +46 -0
  188. package/src/user_data_portability 2.py +328 -0
  189. package/src/user_state_model 2.py +170 -0
  190. package/templates/CLAUDE.md 2.template +108 -0
  191. package/templates/CODEX.AGENTS.md 2.template +66 -0
  192. package/templates/launchagents/README 2.md +132 -0
  193. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
  194. package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
  195. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
  196. package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
  197. package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
  198. package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
  199. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
  200. package/templates/launchagents/com.nexo.immune 2.plist +41 -0
  201. package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
  202. package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
  203. package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
  204. package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
  205. package/templates/nexo_helper 2.py +301 -0
  206. package/templates/openclaw 2.json +13 -0
  207. package/templates/plugin-template 2.py +40 -0
  208. package/templates/script-template 2.py +59 -0
  209. package/templates/script-template 2.sh +13 -0
  210. package/templates/skill-script-template 2.py +48 -0
  211. package/templates/skill-template 2.md +33 -0
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NEXO Proactive Dashboard — Surfaces issues and opportunities without the user asking.
4
+
5
+ Scans: overdue followups, forgotten reminders, unresolved learnings,
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_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
23
+
24
+ NEXO_DB = NEXO_HOME / "data" / "nexo.db"
25
+ INACTIVE_STATUSES = {"DELETED", "ARCHIVED", "BLOCKED", "WAITING", "CANCELLED"}
26
+
27
+
28
+ def get_db():
29
+ conn = sqlite3.connect(str(NEXO_DB), timeout=10)
30
+ conn.row_factory = sqlite3.Row
31
+ return conn
32
+
33
+
34
+ def _is_open_status(status: object) -> bool:
35
+ normalized = str(status or "").strip().upper()
36
+ if normalized.startswith("COMPLETED"):
37
+ return False
38
+ return normalized not in INACTIVE_STATUSES
39
+
40
+
41
+ def check_overdue_followups() -> list[dict]:
42
+ """Find followups that are overdue and not completed."""
43
+ conn = get_db()
44
+ rows = conn.execute("""
45
+ SELECT id, description, date, created_at, reasoning, status
46
+ FROM followups
47
+ WHERE date IS NOT NULL AND date != ''
48
+ ORDER BY date ASC
49
+ """).fetchall()
50
+ conn.close()
51
+ alerts = []
52
+ for r in rows:
53
+ if not _is_open_status(r["status"]):
54
+ continue
55
+ due_str = r["date"]
56
+ try:
57
+ due = datetime.fromisoformat(due_str) if due_str else None
58
+ if due and due < datetime.now():
59
+ days_overdue = (datetime.now() - due).days
60
+ alerts.append({
61
+ "type": "overdue_followup",
62
+ "severity": "high" if days_overdue > 3 else "medium",
63
+ "title": f"Followup overdue by {days_overdue}d: {r['description'][:80]}",
64
+ "id": r["id"],
65
+ "days_overdue": days_overdue,
66
+ })
67
+ except (ValueError, TypeError):
68
+ pass
69
+ return alerts
70
+
71
+
72
+ def check_overdue_reminders() -> list[dict]:
73
+ """Find reminders that are overdue."""
74
+ conn = get_db()
75
+ rows = conn.execute("""
76
+ SELECT id, description, date, status
77
+ FROM reminders
78
+ WHERE date IS NOT NULL AND date != ''
79
+ ORDER BY date ASC
80
+ """).fetchall()
81
+ conn.close()
82
+ alerts = []
83
+ for r in rows:
84
+ if not _is_open_status(r["status"]):
85
+ continue
86
+ due_str = r["date"]
87
+ try:
88
+ due = datetime.fromisoformat(due_str) if due_str else None
89
+ if due and due < datetime.now():
90
+ days_overdue = (datetime.now() - due).days
91
+ alerts.append({
92
+ "type": "overdue_reminder",
93
+ "severity": "high" if days_overdue > 7 else "medium",
94
+ "title": f"Reminder overdue by {days_overdue}d: {r['description'][:80]}",
95
+ "id": r["id"],
96
+ "days_overdue": days_overdue,
97
+ })
98
+ except (ValueError, TypeError):
99
+ pass
100
+ return alerts
101
+
102
+
103
+ def check_stale_ideas() -> list[dict]:
104
+ """Find reminders/ideas without due dates that have been sitting for too long."""
105
+ conn = get_db()
106
+ rows = conn.execute("""
107
+ SELECT id, description, created_at, status
108
+ FROM reminders
109
+ WHERE date IS NULL OR date = ''
110
+ ORDER BY created_at ASC
111
+ """).fetchall()
112
+ conn.close()
113
+ alerts = []
114
+ stale_count = 0
115
+ for r in rows:
116
+ if not _is_open_status(r["status"]):
117
+ continue
118
+ try:
119
+ # created_at is epoch float
120
+ created = datetime.fromtimestamp(r["created_at"])
121
+ age_days = (datetime.now() - created).days
122
+ except (ValueError, TypeError, OSError):
123
+ age_days = 0
124
+ if age_days > 14:
125
+ stale_count += 1
126
+
127
+ if stale_count > 10:
128
+ alerts.append({
129
+ "type": "stale_ideas",
130
+ "severity": "low",
131
+ "title": f"{stale_count} ideas/reminders without date have been sitting for >14 days. Review or archive.",
132
+ "count": stale_count,
133
+ })
134
+ return alerts
135
+
136
+
137
+ def check_session_gaps() -> list[dict]:
138
+ """Detect if NEXO hasn't been active for unusual periods."""
139
+ conn = get_db()
140
+ row = conn.execute("""
141
+ SELECT MAX(created_at) as last_diary FROM session_diary
142
+ """).fetchone()
143
+ conn.close()
144
+ alerts = []
145
+ if row and row["last_diary"]:
146
+ try:
147
+ last = datetime.fromisoformat(row["last_diary"])
148
+ gap_hours = (datetime.now() - last).total_seconds() / 3600
149
+ if gap_hours > 48:
150
+ alerts.append({
151
+ "type": "session_gap",
152
+ "severity": "low",
153
+ "title": f"No sessions recorded in {gap_hours:.0f}h ({gap_hours/24:.1f} days)",
154
+ "gap_hours": gap_hours,
155
+ })
156
+ except (ValueError, TypeError):
157
+ pass
158
+ return alerts
159
+
160
+
161
+ def check_evolution_status() -> list[dict]:
162
+ """Check if evolution system is healthy."""
163
+ alerts = []
164
+ obj_file = NEXO_HOME / "brain" / "evolution-objective.json"
165
+ if obj_file.exists():
166
+ obj = json.loads(obj_file.read_text())
167
+ if not obj.get("evolution_enabled", True):
168
+ alerts.append({
169
+ "type": "evolution_disabled",
170
+ "severity": "high",
171
+ "title": f"Evolution DISABLED: {obj.get('disabled_reason', 'unknown')}",
172
+ })
173
+ if obj.get("consecutive_failures", 0) > 0:
174
+ alerts.append({
175
+ "type": "evolution_failures",
176
+ "severity": "medium",
177
+ "title": f"Evolution: {obj['consecutive_failures']} consecutive failures",
178
+ })
179
+
180
+ # Check dimension regression
181
+ for dim, data in obj.get("dimensions", {}).items():
182
+ current = data.get("current", 0)
183
+ if current < 30:
184
+ alerts.append({
185
+ "type": "dimension_low",
186
+ "severity": "medium",
187
+ "title": f"Dimension '{dim}' baja: {current}%",
188
+ "dimension": dim,
189
+ "score": current,
190
+ })
191
+ return alerts
192
+
193
+
194
+ def check_pending_proposals() -> list[dict]:
195
+ """Check for evolution proposals awaiting the user's review."""
196
+ conn = get_db()
197
+ rows = conn.execute("""
198
+ SELECT id, dimension, proposal, created_at
199
+ FROM evolution_log
200
+ WHERE status = 'proposed' AND classification = 'propose'
201
+ ORDER BY created_at DESC
202
+ """).fetchall()
203
+ conn.close()
204
+ if rows:
205
+ return [{
206
+ "type": "pending_proposals",
207
+ "severity": "low",
208
+ "title": f"{len(rows)} evolution proposals pending review",
209
+ "count": len(rows),
210
+ "proposals": [{"id": r["id"], "dim": r["dimension"], "text": r["proposal"][:80]} for r in rows],
211
+ }]
212
+ return []
213
+
214
+
215
+ def check_recurring_errors() -> list[dict]:
216
+ """Detect learnings that keep appearing (same issue reported multiple times)."""
217
+ conn = get_db()
218
+ rows = conn.execute("""
219
+ SELECT category, COUNT(*) as cnt
220
+ FROM learnings
221
+ WHERE created_at > datetime('now', '-7 days')
222
+ GROUP BY category
223
+ HAVING cnt >= 5
224
+ ORDER BY cnt DESC
225
+ """).fetchall()
226
+ conn.close()
227
+ alerts = []
228
+ for r in rows:
229
+ alerts.append({
230
+ "type": "recurring_errors",
231
+ "severity": "medium",
232
+ "title": f"Category '{r['category']}' has {r['cnt']} learnings this week — possible systemic issue",
233
+ "category": r["category"],
234
+ "count": r["cnt"],
235
+ })
236
+ return alerts
237
+
238
+
239
+ def check_cron_health() -> list[dict]:
240
+ """Check if critical cron jobs are running."""
241
+ alerts = []
242
+
243
+ # Check backup cron
244
+ backup_dir = NEXO_HOME / "backups"
245
+ if backup_dir.exists():
246
+ backups = sorted(backup_dir.glob("nexo-*.db"), key=lambda p: p.stat().st_mtime, reverse=True)
247
+ if backups:
248
+ last_backup_age = (datetime.now().timestamp() - backups[0].stat().st_mtime) / 3600
249
+ if last_backup_age > 4:
250
+ alerts.append({
251
+ "type": "backup_stale",
252
+ "severity": "high",
253
+ "title": f"Last nexo.db backup {last_backup_age:.1f}h (should be hourly)",
254
+ })
255
+
256
+ # Check immune system
257
+ immune_status = NEXO_HOME / "coordination" / "immune-status.json"
258
+ if immune_status.exists():
259
+ try:
260
+ status = json.loads(immune_status.read_text())
261
+ if status.get("status") == "degraded":
262
+ alerts.append({
263
+ "type": "immune_degraded",
264
+ "severity": "high",
265
+ "title": f"Immune system degraded: {status.get('reason', '?')}",
266
+ })
267
+ except (json.JSONDecodeError, KeyError):
268
+ pass
269
+
270
+ return alerts
271
+
272
+
273
+ def run_all_checks() -> list[dict]:
274
+ """Run all proactive checks and return sorted alerts."""
275
+ all_alerts = []
276
+ checks = [
277
+ check_overdue_followups,
278
+ check_overdue_reminders,
279
+ check_stale_ideas,
280
+ check_session_gaps,
281
+ check_evolution_status,
282
+ check_pending_proposals,
283
+ check_recurring_errors,
284
+ check_cron_health,
285
+ ]
286
+
287
+ for check in checks:
288
+ try:
289
+ all_alerts.extend(check())
290
+ except Exception as e:
291
+ all_alerts.append({
292
+ "type": "check_error",
293
+ "severity": "low",
294
+ "title": f"Check {check.__name__} failed: {e}",
295
+ })
296
+
297
+ # Sort by severity
298
+ severity_order = {"high": 0, "medium": 1, "low": 2}
299
+ all_alerts.sort(key=lambda a: severity_order.get(a.get("severity", "low"), 3))
300
+
301
+ return all_alerts
302
+
303
+
304
+ def format_text(alerts: list[dict]) -> str:
305
+ """Format alerts as readable text."""
306
+ if not alerts:
307
+ return "No proactive alerts. All clear."
308
+
309
+ severity_icons = {"high": "!!!", "medium": " ! ", "low": " . "}
310
+ lines = [f"NEXO Proactive Dashboard — {len(alerts)} alerts\n"]
311
+
312
+ current_severity = None
313
+ for a in alerts:
314
+ sev = a.get("severity", "low")
315
+ if sev != current_severity:
316
+ current_severity = sev
317
+ label = {"high": "URGENTE", "medium": "ATENCION", "low": "INFO"}.get(sev, sev)
318
+ lines.append(f"\n [{label}]")
319
+ icon = severity_icons.get(sev, " . ")
320
+ lines.append(f" {icon} {a['title']}")
321
+
322
+ return "\n".join(lines)
323
+
324
+
325
+ def format_brief(alerts: list[dict]) -> str:
326
+ """One-liner summary."""
327
+ high = sum(1 for a in alerts if a.get("severity") == "high")
328
+ med = sum(1 for a in alerts if a.get("severity") == "medium")
329
+ low = sum(1 for a in alerts if a.get("severity") == "low")
330
+ if not alerts:
331
+ return "Dashboard: clean"
332
+ return f"Dashboard: {high} urgent, {med} attention, {low} info"
333
+
334
+
335
+ def main():
336
+ output_json = "--json" in sys.argv
337
+ brief = "--brief" in sys.argv
338
+
339
+ alerts = run_all_checks()
340
+
341
+ if output_json:
342
+ print(json.dumps(alerts, indent=2, default=str))
343
+ elif brief:
344
+ print(format_brief(alerts))
345
+ else:
346
+ print(format_text(alerts))
347
+
348
+ # Exit code = number of high severity alerts
349
+ high_count = sum(1 for a in alerts if a.get("severity") == "high")
350
+ sys.exit(min(high_count, 125))
351
+
352
+
353
+ if __name__ == "__main__":
354
+ main()
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NEXO Reflection Engine — Processes session_buffer.jsonl entries.
4
+
5
+ Triggered by the stop hook when >=3 sessions have accumulated and
6
+ the last reflection was >4 hours ago.
7
+
8
+ What it does:
9
+ 1. Reads all entries from session_buffer.jsonl
10
+ 2. Extracts patterns: recurring tasks, common errors, user mood trends
11
+ 3. Updates user_model.json with observed patterns
12
+ 4. Writes a reflection summary to reflection-log.json
13
+ 5. Clears processed entries from the buffer
14
+
15
+ Runs as a standalone Python script (no LLM needed).
16
+ """
17
+
18
+ import json
19
+ import os
20
+ import sys
21
+ from datetime import datetime
22
+ from collections import Counter
23
+
24
+ NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
25
+ BUFFER_PATH = os.path.join(NEXO_HOME, "brain", "session_buffer.jsonl")
26
+ USER_MODEL_PATH = os.path.join(NEXO_HOME, "brain", "user_model.json")
27
+ REFLECTION_LOG_PATH = os.path.join(NEXO_HOME, "coordination", "reflection-log.json")
28
+
29
+
30
+ def load_buffer():
31
+ """Load all entries from session buffer."""
32
+ entries = []
33
+ if not os.path.exists(BUFFER_PATH):
34
+ return entries
35
+ with open(BUFFER_PATH, "r") as f:
36
+ for line in f:
37
+ line = line.strip()
38
+ if not line:
39
+ continue
40
+ try:
41
+ entries.append(json.loads(line))
42
+ except json.JSONDecodeError:
43
+ continue
44
+ return entries
45
+
46
+
47
+ def load_user_model():
48
+ """Load existing user model or create empty one."""
49
+ if os.path.exists(USER_MODEL_PATH):
50
+ try:
51
+ return json.load(open(USER_MODEL_PATH))
52
+ except (json.JSONDecodeError, IOError):
53
+ pass
54
+ return {
55
+ "created": datetime.now().strftime("%Y-%m-%d"),
56
+ "traits": {},
57
+ "work_patterns": {},
58
+ "mood_history": [],
59
+ "common_tasks": [],
60
+ "error_patterns": [],
61
+ "reflections_count": 0,
62
+ }
63
+
64
+
65
+ def load_reflection_log():
66
+ """Load existing reflection log."""
67
+ if os.path.exists(REFLECTION_LOG_PATH):
68
+ try:
69
+ return json.load(open(REFLECTION_LOG_PATH))
70
+ except (json.JSONDecodeError, IOError):
71
+ pass
72
+ return []
73
+
74
+
75
+ def analyze_entries(entries):
76
+ """Extract patterns from buffer entries."""
77
+ tasks = []
78
+ decisions = []
79
+ errors = []
80
+ moods = Counter()
81
+ user_patterns = []
82
+ files_modified = []
83
+ self_critiques = []
84
+
85
+ for entry in entries:
86
+ source = entry.get("source", "")
87
+ # Skip hook-fallback entries (they have no real data)
88
+ if source == "hook-fallback":
89
+ continue
90
+
91
+ # Collect tasks
92
+ for t in entry.get("tasks", []):
93
+ if t and t != "session ended":
94
+ tasks.append(t)
95
+
96
+ # Collect decisions
97
+ for d in entry.get("decisions", []):
98
+ if d:
99
+ decisions.append(d)
100
+
101
+ # Collect errors
102
+ for e in entry.get("errors_resolved", []):
103
+ if e:
104
+ errors.append(e)
105
+
106
+ # Count moods
107
+ mood = entry.get("mood", "unknown")
108
+ if mood and mood != "unknown":
109
+ moods[mood] += 1
110
+
111
+ # User patterns
112
+ for p in entry.get("user_patterns", []):
113
+ if p:
114
+ user_patterns.append(p)
115
+
116
+ # Files
117
+ for f in entry.get("files_modified", []):
118
+ if f:
119
+ files_modified.append(f)
120
+
121
+ # Self-critiques
122
+ critique = entry.get("self_critique", "")
123
+ if critique and "hook-fallback" not in critique:
124
+ self_critiques.append(critique)
125
+
126
+ return {
127
+ "tasks": tasks,
128
+ "decisions": decisions,
129
+ "errors": errors,
130
+ "moods": dict(moods),
131
+ "user_patterns": user_patterns,
132
+ "files_modified": files_modified,
133
+ "self_critiques": self_critiques,
134
+ "entry_count": len(entries),
135
+ "source_counts": dict(Counter(e.get("source") or "unknown" for e in entries)),
136
+ }
137
+
138
+
139
+ def update_user_model(model, analysis):
140
+ """Update user model with new patterns."""
141
+ # Update mood history (keep last 50)
142
+ if analysis["moods"]:
143
+ model["mood_history"].append({
144
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
145
+ "moods": analysis["moods"],
146
+ })
147
+ model["mood_history"] = model["mood_history"][-50:]
148
+
149
+ # Update common tasks (top 20)
150
+ task_counter = Counter(model.get("common_tasks", []))
151
+ task_counter.update(analysis["tasks"])
152
+ model["common_tasks"] = [t for t, _ in task_counter.most_common(20)]
153
+
154
+ # Update error patterns (keep last 30)
155
+ existing_errors = model.get("error_patterns", [])
156
+ existing_errors.extend(analysis["errors"])
157
+ model["error_patterns"] = existing_errors[-30:]
158
+
159
+ # Update work patterns
160
+ if analysis["files_modified"]:
161
+ file_counter = Counter(model.get("work_patterns", {}).get("frequent_files", []))
162
+ file_counter.update(analysis["files_modified"])
163
+ if "work_patterns" not in model:
164
+ model["work_patterns"] = {}
165
+ model["work_patterns"]["frequent_files"] = [
166
+ f for f, _ in file_counter.most_common(20)
167
+ ]
168
+
169
+ # Derive traits from mood patterns
170
+ total_moods = sum(analysis["moods"].values())
171
+ if total_moods >= 3:
172
+ dominant_mood = max(analysis["moods"], key=analysis["moods"].get)
173
+ model["traits"]["recent_dominant_mood"] = dominant_mood
174
+ model["traits"]["mood_updated"] = datetime.now().strftime("%Y-%m-%d")
175
+
176
+ model["reflections_count"] = model.get("reflections_count", 0) + 1
177
+ model["last_reflection"] = datetime.now().strftime("%Y-%m-%d %H:%M")
178
+
179
+ return model
180
+
181
+
182
+ def main():
183
+ entries = load_buffer()
184
+ if not entries:
185
+ print("No entries in buffer.")
186
+ return
187
+
188
+ # Filter out pure hook entries (tool captures)
189
+ session_entries = [
190
+ e for e in entries
191
+ if e.get("source") in ("claude", "hook-fallback", "codex", "codex_cli", "codex_cli_rs")
192
+ or "tasks" in e
193
+ ]
194
+
195
+ if not session_entries:
196
+ print("No session entries to process.")
197
+ return
198
+
199
+ analysis = analyze_entries(session_entries)
200
+ model = load_user_model()
201
+ model = update_user_model(model, analysis)
202
+
203
+ # Save updated model
204
+ os.makedirs(os.path.dirname(USER_MODEL_PATH), exist_ok=True)
205
+ with open(USER_MODEL_PATH, "w") as f:
206
+ json.dump(model, f, indent=2)
207
+
208
+ # Append to reflection log
209
+ log = load_reflection_log()
210
+ reflection = {
211
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
212
+ "entries_processed": analysis["entry_count"],
213
+ "source_counts": analysis["source_counts"],
214
+ "tasks_seen": len(analysis["tasks"]),
215
+ "decisions_made": len(analysis["decisions"]),
216
+ "errors_resolved": len(analysis["errors"]),
217
+ "mood_distribution": analysis["moods"],
218
+ "self_critiques": analysis["self_critiques"][:5],
219
+ "user_patterns_observed": analysis["user_patterns"][:5],
220
+ }
221
+ log.append(reflection)
222
+ # Keep last 100 reflections
223
+ log = log[-100:]
224
+
225
+ os.makedirs(os.path.dirname(REFLECTION_LOG_PATH), exist_ok=True)
226
+ with open(REFLECTION_LOG_PATH, "w") as f:
227
+ json.dump(log, f, indent=2)
228
+
229
+ # Clear buffer (processed)
230
+ with open(BUFFER_PATH, "w") as f:
231
+ f.write("")
232
+
233
+ # Adaptive personality: decay inter-session tension
234
+ try:
235
+ import sys
236
+ sys.path.insert(0, os.path.join(NEXO_HOME))
237
+ from plugins.adaptive_mode import decay_tension
238
+ decay_result = decay_tension()
239
+ if decay_result:
240
+ reflection["adaptive_decay"] = decay_result
241
+ print(f"Adaptive decay: tension {decay_result['old_tension']} → {decay_result['new_tension']} "
242
+ f"({decay_result['hours_elapsed']}h elapsed)")
243
+ except Exception as e:
244
+ print(f"Adaptive decay skipped: {e}")
245
+
246
+ source_summary = ", ".join(
247
+ f"{source}={count}" for source, count in sorted(analysis["source_counts"].items())
248
+ ) or "none"
249
+ print(f"Reflection complete: {analysis['entry_count']} entries processed, "
250
+ f"sources: {source_summary}, "
251
+ f"{len(analysis['errors'])} errors, "
252
+ f"{len(analysis['self_critiques'])} critiques.")
253
+
254
+
255
+ if __name__ == "__main__":
256
+ main()