nexo-brain 5.3.26 → 5.3.28

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