nexo-brain 5.3.26 → 5.3.27

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/package.json +1 -1
  3. package/src/server.py +3 -0
  4. package/src/tools_sessions.py +6 -1
  5. package/src/dashboard/static/favicon 2.svg +0 -32
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +0 -40
  8. package/src/dashboard/static/style 2.css +0 -2458
  9. package/src/dashboard/templates/adaptive 2.html +0 -118
  10. package/src/dashboard/templates/artifacts 2.html +0 -133
  11. package/src/dashboard/templates/backups 2.html +0 -136
  12. package/src/dashboard/templates/base 2.html +0 -417
  13. package/src/dashboard/templates/calendar 2.html +0 -591
  14. package/src/dashboard/templates/chat 2.html +0 -356
  15. package/src/dashboard/templates/claims 2.html +0 -259
  16. package/src/dashboard/templates/cortex 2.html +0 -321
  17. package/src/dashboard/templates/credentials 2.html +0 -128
  18. package/src/dashboard/templates/crons 2.html +0 -370
  19. package/src/dashboard/templates/dashboard 2.html +0 -494
  20. package/src/dashboard/templates/dreams 2.html +0 -252
  21. package/src/dashboard/templates/email 2.html +0 -160
  22. package/src/dashboard/templates/evolution 2.html +0 -189
  23. package/src/dashboard/templates/feed 2.html +0 -249
  24. package/src/dashboard/templates/followup_health 2.html +0 -170
  25. package/src/dashboard/templates/graph 2.html +0 -201
  26. package/src/dashboard/templates/guard 2.html +0 -259
  27. package/src/dashboard/templates/inbox 2.html +0 -251
  28. package/src/dashboard/templates/memory 2.html +0 -420
  29. package/src/dashboard/templates/operations 2.html +0 -608
  30. package/src/dashboard/templates/plugins 2.html +0 -185
  31. package/src/dashboard/templates/protocol 2.html +0 -199
  32. package/src/dashboard/templates/rules 2.html +0 -246
  33. package/src/dashboard/templates/sentiment 2.html +0 -247
  34. package/src/dashboard/templates/sessions 2.html +0 -218
  35. package/src/dashboard/templates/skills 2.html +0 -329
  36. package/src/dashboard/templates/somatic 2.html +0 -73
  37. package/src/dashboard/templates/triggers 2.html +0 -133
  38. package/src/dashboard/templates/trust 2.html +0 -360
  39. package/src/db/__init__ 2.py +0 -259
  40. package/src/db/_core 2.py +0 -437
  41. package/src/db/_credentials 2.py +0 -124
  42. package/src/db/_episodic 2.py +0 -762
  43. package/src/db/_evolution 2.py +0 -54
  44. package/src/db/_fts 2.py +0 -406
  45. package/src/db/_goal_profiles 2.py +0 -376
  46. package/src/db/_hot_context 2.py +0 -660
  47. package/src/db/_outcomes 2.py +0 -800
  48. package/src/db/_personal_scripts 2.py +0 -582
  49. package/src/db/_sessions 2.py +0 -330
  50. package/src/db/_tasks 2.py +0 -91
  51. package/src/db/_watchers 2.py +0 -173
  52. package/src/doctor/formatters 2.py +0 -52
  53. package/src/doctor/models 2.py +0 -69
  54. package/src/doctor/planes 2.py +0 -87
  55. package/src/doctor/providers/__init__ 2.py +0 -1
  56. package/src/doctor/providers/deep 2.py +0 -367
  57. package/src/evolution_cycle 2.py +0 -519
  58. package/src/hooks/auto_capture 2.py +0 -208
  59. package/src/hooks/caffeinate-guard 2.sh +0 -8
  60. package/src/hooks/capture-session 2.sh +0 -21
  61. package/src/hooks/capture-tool-logs 2.sh +0 -158
  62. package/src/hooks/daily-briefing-check 2.sh +0 -33
  63. package/src/hooks/heartbeat-enforcement 2.py +0 -90
  64. package/src/hooks/heartbeat-posttool 2.sh +0 -18
  65. package/src/hooks/inbox-hook 2.sh +0 -76
  66. package/src/hooks/post-compact 2.sh +0 -152
  67. package/src/hooks/pre-compact 2.sh +0 -169
  68. package/src/hooks/protocol-guardrail 2.sh +0 -10
  69. package/src/hooks/protocol-pretool-guardrail 2.sh +0 -9
  70. package/src/hooks/session-stop 2.sh +0 -52
  71. package/src/kg_populate 2.py +0 -292
  72. package/src/maintenance 2.py +0 -53
  73. package/src/memory_backends 2.py +0 -71
  74. package/src/migrate_embeddings 2.py +0 -124
  75. package/src/nexo_sdk 2.py +0 -103
  76. package/src/observability 2.py +0 -199
  77. package/src/plugin_loader 2.py +0 -217
  78. package/src/plugins/__init__ 2.py +0 -0
  79. package/src/plugins/artifact_registry 2.py +0 -450
  80. package/src/plugins/backup 2.py +0 -127
  81. package/src/plugins/claims_tools 2.py +0 -119
  82. package/src/plugins/cognitive_memory 2.py +0 -609
  83. package/src/plugins/core_rules 2.py +0 -252
  84. package/src/plugins/cortex 2.py +0 -1155
  85. package/src/plugins/entities 2.py +0 -67
  86. package/src/plugins/episodic_memory 2.py +0 -560
  87. package/src/plugins/evolution 2.py +0 -167
  88. package/src/plugins/goal_engine 2.py +0 -142
  89. package/src/plugins/guard 2.py +0 -862
  90. package/src/plugins/impact 2.py +0 -29
  91. package/src/plugins/knowledge_graph_tools 2.py +0 -137
  92. package/src/plugins/media_memory_tools 2.py +0 -98
  93. package/src/plugins/memory_export 2.py +0 -196
  94. package/src/plugins/outcomes 2.py +0 -130
  95. package/src/plugins/personal_scripts 2.py +0 -117
  96. package/src/plugins/preferences 2.py +0 -47
  97. package/src/plugins/protocol 2.py +0 -1449
  98. package/src/plugins/simple_api 2.py +0 -106
  99. package/src/plugins/skills 2.py +0 -341
  100. package/src/plugins/state_watchers 2.py +0 -79
  101. package/src/plugins/update 2.py +0 -986
  102. package/src/plugins/user_state_tools 2.py +0 -43
  103. package/src/plugins/workflow 2.py +0 -588
  104. package/src/protocol_settings 2.py +0 -59
  105. package/src/public_contribution 2.py +0 -466
  106. package/src/public_evolution_queue 2.py +0 -241
  107. package/src/requirements 2.txt +0 -14
  108. package/src/retroactive_learnings 2.py +0 -373
  109. package/src/rules/__init__ 2.py +0 -0
  110. package/src/rules/core-rules 2.json +0 -331
  111. package/src/rules/migrate 2.py +0 -207
  112. package/src/runtime_power 2.py +0 -874
  113. package/src/script_registry 2.py +0 -1559
  114. package/src/scripts/check-context 2.py +0 -272
  115. package/src/scripts/deep-sleep/apply_findings 2.py +0 -2327
  116. package/src/scripts/deep-sleep/collect 2.py +0 -928
  117. package/src/scripts/deep-sleep/extract 2.py +0 -330
  118. package/src/scripts/deep-sleep/extract-prompt 2.md +0 -285
  119. package/src/scripts/deep-sleep/synthesize 2.py +0 -312
  120. package/src/scripts/deep-sleep/synthesize-prompt 2.md +0 -336
  121. package/src/scripts/nexo-agent-run 2.py +0 -75
  122. package/src/scripts/nexo-auto-update 2.py +0 -6
  123. package/src/scripts/nexo-backup 2.sh +0 -25
  124. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  125. package/src/scripts/nexo-catchup 2.py +0 -300
  126. package/src/scripts/nexo-cognitive-decay 2.py +0 -257
  127. package/src/scripts/nexo-cortex-cycle 2.py +0 -293
  128. package/src/scripts/nexo-cron-wrapper 2.sh +0 -53
  129. package/src/scripts/nexo-daily-self-audit 2.py +0 -2161
  130. package/src/scripts/nexo-dashboard 2.sh +0 -29
  131. package/src/scripts/nexo-deep-sleep 2.sh +0 -86
  132. package/src/scripts/nexo-evolution-run 2.py +0 -1664
  133. package/src/scripts/nexo-followup-hygiene 2.py +0 -139
  134. package/src/scripts/nexo-hook-record 2.py +0 -42
  135. package/src/scripts/nexo-immune 2.py +0 -936
  136. package/src/scripts/nexo-impact-scorer 2.py +0 -117
  137. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  138. package/src/scripts/nexo-install 2.py +0 -6
  139. package/src/scripts/nexo-learning-housekeep 2.py +0 -401
  140. package/src/scripts/nexo-learning-validator 2.py +0 -266
  141. package/src/scripts/nexo-migrate 2.py +0 -260
  142. package/src/scripts/nexo-outcome-checker 2.py +0 -127
  143. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -456
  144. package/src/scripts/nexo-pre-commit 2.py +0 -120
  145. package/src/scripts/nexo-prevent-sleep 2.sh +0 -35
  146. package/src/scripts/nexo-proactive-dashboard 2.py +0 -354
  147. package/src/scripts/nexo-reflection 2.py +0 -256
  148. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  149. package/src/scripts/nexo-sleep 2.py +0 -631
  150. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  151. package/src/scripts/nexo-sync-clients 2.py +0 -16
  152. package/src/scripts/nexo-synthesis 2.py +0 -475
  153. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  154. package/src/scripts/nexo-update 2.sh +0 -306
  155. package/src/scripts/nexo-watchdog 2.sh +0 -1207
  156. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  157. package/src/scripts/rehydrate_learnings_from_archive 2.py +0 -245
  158. package/src/server 2.py +0 -1296
  159. package/src/skills/run-nexo-audit-phase/guide 2.md +0 -43
  160. package/src/skills/run-nexo-audit-phase/skill 2.json +0 -59
  161. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +0 -17
  162. package/src/skills/run-nexo-core-fix-cycle/script 2.py +0 -276
  163. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +0 -58
  164. package/src/skills/run-release-final-audit/guide 2.md +0 -16
  165. package/src/skills/run-release-final-audit/script 2.py +0 -259
  166. package/src/skills/run-release-final-audit/skill 2.json +0 -77
  167. package/src/skills/run-runtime-doctor/guide 2.md +0 -12
  168. package/src/skills/run-runtime-doctor/script 2.py +0 -21
  169. package/src/skills/run-runtime-doctor/skill 2.json +0 -25
  170. package/src/skills_runtime 2.py +0 -932
  171. package/src/state_watchers_runtime 2.py +0 -475
  172. package/src/storage_router 2.py +0 -32
  173. package/src/system_catalog 2.py +0 -786
  174. package/src/tools_coordination 2.py +0 -103
  175. package/src/tools_credentials 2.py +0 -68
  176. package/src/tools_drive 2.py +0 -487
  177. package/src/tools_hot_context 2.py +0 -163
  178. package/src/tools_learnings 2.py +0 -612
  179. package/src/tools_menu 2.py +0 -229
  180. package/src/tools_reminders 2.py +0 -88
  181. package/src/tools_reminders_crud 2.py +0 -363
  182. package/src/tools_sessions 2.py +0 -1054
  183. package/src/tools_system_catalog 2.py +0 -19
  184. package/src/tools_task_history 2.py +0 -57
  185. package/src/tools_transcripts 2.py +0 -98
  186. package/src/transcript_utils 2.py +0 -412
  187. package/src/user_context 2.py +0 -46
  188. package/src/user_data_portability 2.py +0 -328
  189. package/src/user_state_model 2.py +0 -170
  190. package/templates/CLAUDE.md 2.template +0 -108
  191. package/templates/CODEX.AGENTS.md 2.template +0 -66
  192. package/templates/launchagents/README 2.md +0 -132
  193. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +0 -39
  194. package/templates/launchagents/com.nexo.catchup 2.plist +0 -39
  195. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +0 -40
  196. package/templates/launchagents/com.nexo.dashboard 2.plist +0 -43
  197. package/templates/launchagents/com.nexo.deep-sleep 2.plist +0 -43
  198. package/templates/launchagents/com.nexo.evolution 2.plist +0 -44
  199. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +0 -45
  200. package/templates/launchagents/com.nexo.immune 2.plist +0 -41
  201. package/templates/launchagents/com.nexo.postmortem 2.plist +0 -45
  202. package/templates/launchagents/com.nexo.self-audit 2.plist +0 -47
  203. package/templates/launchagents/com.nexo.synthesis 2.plist +0 -45
  204. package/templates/launchagents/com.nexo.watchdog 2.plist +0 -37
  205. package/templates/nexo_helper 2.py +0 -301
  206. package/templates/openclaw 2.json +0 -13
  207. package/templates/plugin-template 2.py +0 -40
  208. package/templates/script-template 2.py +0 -59
  209. package/templates/script-template 2.sh +0 -13
  210. package/templates/skill-script-template 2.py +0 -48
  211. package/templates/skill-template 2.md +0 -33
@@ -1,354 +0,0 @@
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()
@@ -1,256 +0,0 @@
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()