nexo-brain 2.3.0 → 2.3.2

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 (299) hide show
  1. package/README.md +1 -1
  2. package/bin/nexo-brain.js +92 -9
  3. package/bin/postinstall.js +22 -15
  4. package/package.json +7 -4
  5. package/src/auto_update.py +194 -5
  6. package/src/crons/sync.py +6 -2
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_entities.py +1 -0
  9. package/src/db/_episodic.py +1 -0
  10. package/src/db/_learnings.py +1 -0
  11. package/src/db/_reminders.py +1 -0
  12. package/src/db/_schema.py +11 -1
  13. package/src/db/_sessions.py +1 -0
  14. package/src/db/_skills.py +1 -0
  15. package/src/hooks/capture-tool-logs.sh +23 -6
  16. package/src/hooks/session-start.sh +4 -3
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/update.py +377 -26
  19. package/src/scripts/deep-sleep/apply_findings.py +1 -0
  20. package/src/scripts/deep-sleep/collect.py +1 -0
  21. package/src/scripts/deep-sleep/extract.py +1 -0
  22. package/src/scripts/deep-sleep/synthesize.py +1 -0
  23. package/src/scripts/nexo-catchup.py +29 -4
  24. package/src/scripts/nexo-daily-self-audit.py +21 -1
  25. package/src/scripts/nexo-evolution-run.py +21 -1
  26. package/src/scripts/nexo-learning-housekeep.py +1 -0
  27. package/src/scripts/nexo-postmortem-consolidator.py +34 -9
  28. package/src/scripts/nexo-sleep.py +32 -10
  29. package/src/scripts/nexo-synthesis.py +29 -9
  30. package/src/scripts/nexo-update.sh +109 -7
  31. package/src/scripts/nexo-watchdog.sh +122 -58
  32. package/src/server.py +66 -1
  33. package/src/tools_coordination.py +1 -0
  34. package/src/tools_sessions.py +1 -0
  35. package/scripts/migrate-to-unified 2.sh +0 -813
  36. package/scripts/migrate-to-unified.sh +0 -813
  37. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  38. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  39. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  40. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  41. package/scripts/nexo-preflight.sh +0 -236
  42. package/scripts/pre-commit-check 2.sh +0 -55
  43. package/scripts/pre-commit-check.sh +0 -55
  44. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  45. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  46. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  47. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  48. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  49. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  50. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  51. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  52. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  53. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  54. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  60. package/src/auto_close_sessions 2.py +0 -159
  61. package/src/auto_update 2.py +0 -634
  62. package/src/claim_graph 2.py +0 -323
  63. package/src/cognitive/__init__ 2.py +0 -62
  64. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  65. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  66. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  67. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  69. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  70. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  72. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  73. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  74. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  75. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  76. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  77. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  78. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  79. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  80. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  81. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  82. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  83. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  84. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  85. package/src/cognitive/_core 2.py +0 -567
  86. package/src/cognitive/_decay 2.py +0 -382
  87. package/src/cognitive/_ingest 2.py +0 -892
  88. package/src/cognitive/_memory 2.py +0 -912
  89. package/src/cognitive/_search 2.py +0 -949
  90. package/src/cognitive/_trust 2.py +0 -464
  91. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  92. package/src/crons/manifest 2.json +0 -106
  93. package/src/crons/sync 2.py +0 -217
  94. package/src/dashboard/__init__ 2.py +0 -0
  95. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  96. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  97. package/src/dashboard/app 2.py +0 -789
  98. package/src/db/__init__ 2.py +0 -89
  99. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  110. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  111. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  112. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  113. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  114. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  115. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  116. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  117. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  118. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  119. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  120. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  121. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  122. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  123. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  124. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  125. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  126. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  127. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  128. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  129. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  130. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  131. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  132. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  133. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  134. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  135. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  136. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  137. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  138. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  139. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  140. package/src/db/_core 2.py +0 -417
  141. package/src/db/_credentials 2.py +0 -124
  142. package/src/db/_entities 2.py +0 -178
  143. package/src/db/_episodic 2.py +0 -738
  144. package/src/db/_evolution 2.py +0 -54
  145. package/src/db/_fts 2.py +0 -406
  146. package/src/db/_learnings 2.py +0 -168
  147. package/src/db/_reminders 2.py +0 -338
  148. package/src/db/_schema 2.py +0 -364
  149. package/src/db/_sessions 2.py +0 -300
  150. package/src/db/_tasks 2.py +0 -91
  151. package/src/evolution_cycle 2.py +0 -266
  152. package/src/hnsw_index 2.py +0 -254
  153. package/src/hooks/auto_capture 2.py +0 -208
  154. package/src/hooks/caffeinate-guard 2.sh +0 -8
  155. package/src/hooks/capture-session 2.sh +0 -21
  156. package/src/hooks/capture-tool-logs 2.sh +0 -127
  157. package/src/hooks/daily-briefing-check 2.sh +0 -33
  158. package/src/hooks/inbox-hook 2.sh +0 -76
  159. package/src/hooks/post-compact 2.sh +0 -148
  160. package/src/hooks/pre-compact 2.sh +0 -151
  161. package/src/hooks/session-start 2.sh +0 -268
  162. package/src/hooks/session-stop 2.sh +0 -140
  163. package/src/kg_populate 2.py +0 -290
  164. package/src/knowledge_graph 2.py +0 -257
  165. package/src/maintenance 2.py +0 -59
  166. package/src/migrate_embeddings 2.py +0 -122
  167. package/src/plugin_loader 2.py +0 -202
  168. package/src/plugins/__init__ 2.py +0 -0
  169. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  172. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  175. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  183. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  185. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  187. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  188. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  189. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  190. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  191. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  192. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  193. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  194. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  195. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  196. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  197. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  198. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  199. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  200. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  201. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  202. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  203. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  204. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  205. package/src/plugins/adaptive_mode 2.py +0 -805
  206. package/src/plugins/agents 2.py +0 -52
  207. package/src/plugins/artifact_registry 2.py +0 -450
  208. package/src/plugins/backup 2.py +0 -104
  209. package/src/plugins/cognitive_memory 2.py +0 -564
  210. package/src/plugins/core_rules 2.py +0 -252
  211. package/src/plugins/cortex 2.py +0 -299
  212. package/src/plugins/entities 2.py +0 -67
  213. package/src/plugins/episodic_memory 2.py +0 -533
  214. package/src/plugins/evolution 2.py +0 -115
  215. package/src/plugins/guard 2.py +0 -746
  216. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  217. package/src/plugins/preferences 2.py +0 -47
  218. package/src/plugins/update 2.py +0 -256
  219. package/src/requirements 2.txt +0 -12
  220. package/src/rules/__init__ 2.py +0 -0
  221. package/src/rules/core-rules 2.json +0 -331
  222. package/src/rules/migrate 2.py +0 -207
  223. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  232. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  233. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  234. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  235. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  236. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  237. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  238. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  239. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  240. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  241. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  242. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  243. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  244. package/src/scripts/check-context 2.py +0 -264
  245. package/src/scripts/nexo-auto-update 2.py +0 -6
  246. package/src/scripts/nexo-backup 2.sh +0 -25
  247. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  248. package/src/scripts/nexo-catchup 2.py +0 -242
  249. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  250. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  251. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  252. package/src/scripts/nexo-evolution-run 2.py +0 -597
  253. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  254. package/src/scripts/nexo-github-monitor 2.py +0 -256
  255. package/src/scripts/nexo-immune 2.py +0 -927
  256. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  257. package/src/scripts/nexo-install 2.py +0 -6
  258. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  259. package/src/scripts/nexo-learning-validator 2.py +0 -207
  260. package/src/scripts/nexo-migrate 2.py +0 -232
  261. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  262. package/src/scripts/nexo-pre-commit 2.py +0 -120
  263. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  264. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  265. package/src/scripts/nexo-reflection 2.py +0 -253
  266. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  267. package/src/scripts/nexo-send-email 2.py +0 -25
  268. package/src/scripts/nexo-send-email.py +0 -25
  269. package/src/scripts/nexo-send-reply 2.py +0 -178
  270. package/src/scripts/nexo-send-reply.py +0 -178
  271. package/src/scripts/nexo-sleep 2.py +0 -592
  272. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  273. package/src/scripts/nexo-synthesis 2.py +0 -253
  274. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  275. package/src/scripts/nexo-update 2.sh +0 -161
  276. package/src/scripts/nexo-watchdog 2.sh +0 -878
  277. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  278. package/src/server 2.py +0 -733
  279. package/src/storage_router 2.py +0 -32
  280. package/src/tools_coordination 2.py +0 -102
  281. package/src/tools_credentials 2.py +0 -68
  282. package/src/tools_learnings 2.py +0 -220
  283. package/src/tools_menu 2.py +0 -227
  284. package/src/tools_reminders 2.py +0 -86
  285. package/src/tools_reminders_crud 2.py +0 -159
  286. package/src/tools_sessions 2.py +0 -476
  287. package/src/tools_task_history 2.py +0 -57
  288. package/templates/CLAUDE.md 2.template +0 -63
  289. package/templates/openclaw 2.json +0 -13
  290. package/tests/__init__ 2.py +0 -0
  291. package/tests/__init__.py +0 -0
  292. package/tests/conftest 2.py +0 -71
  293. package/tests/conftest.py +0 -71
  294. package/tests/test_cognitive 2.py +0 -205
  295. package/tests/test_cognitive.py +0 -205
  296. package/tests/test_knowledge_graph 2.py +0 -140
  297. package/tests/test_knowledge_graph.py +0 -140
  298. package/tests/test_migrations 2.py +0 -137
  299. package/tests/test_migrations.py +0 -137
@@ -1,597 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- NEXO Evolution — Standalone weekly runner with real execution.
4
- Cron: 0 3 * * 0 (Sundays 3:00 AM)
5
-
6
- Runs independently of Cortex. Calls Opus API directly to analyze
7
- the past week and generate improvement proposals.
8
-
9
- AUTO proposals are executed: snapshot → apply → validate → commit/rollback.
10
- PROPOSE proposals are logged for the user's review.
11
- """
12
-
13
- import json
14
- import os
15
- import py_compile
16
- import sqlite3
17
- import subprocess
18
- import sys
19
- from datetime import datetime, date, timedelta
20
- from pathlib import Path
21
-
22
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
23
- # Auto-detect: if running from repo (src/scripts/), use src/ as NEXO_CODE
24
- _script_dir = Path(__file__).resolve().parent
25
- _repo_src = _script_dir.parent # src/scripts/ -> src/
26
- NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
27
-
28
- # ── Paths ────────────────────────────────────────────────────────────────
29
- CLAUDE_DIR = NEXO_HOME
30
- NEXO_DB = CLAUDE_DIR / "data" / "nexo.db"
31
- LOG_DIR = CLAUDE_DIR / "logs"
32
- SNAPSHOTS_DIR = CLAUDE_DIR / "snapshots"
33
- SANDBOX_DIR = CLAUDE_DIR / "sandbox" / "workspace"
34
- MAX_CONSECUTIVE_FAILURES = 3
35
- MAX_SNAPSHOTS = 8
36
-
37
- # ── Safe zones for AUTO execution ────────────────────────────────────────
38
- # "review" mode (owner): broader zones, but nothing executes without approval
39
- # "auto" mode (public users): restricted to user scripts and plugins ONLY
40
- AUTO_SAFE_PREFIXES = [
41
- str(CLAUDE_DIR / "scripts") + "/",
42
- str(CLAUDE_DIR / "brain") + "/",
43
- str(NEXO_CODE / "plugins") + "/",
44
- str(CLAUDE_DIR / "logs") + "/",
45
- str(CLAUDE_DIR / "coordination") + "/",
46
- ]
47
-
48
- # Public mode: only user-created scripts — NEVER core, cortex, or plugins
49
- AUTO_SAFE_PREFIXES_PUBLIC = [
50
- str(CLAUDE_DIR / "scripts") + "/",
51
- ]
52
-
53
- # ── Immutable files — NEVER touch (applies to ALL modes) ────────────────
54
- IMMUTABLE_FILES = {
55
- "db.py", "server.py", "plugin_loader.py", "nexo-watchdog.sh",
56
- "cortex-wrapper.py", "CLAUDE.md", "personality.md",
57
- "user-profile.md", "evolution_cycle.py",
58
- # Core cognitive engine — never auto-modified
59
- "cognitive.py", "knowledge_graph.py", "storage_router.py",
60
- # Core tools — never auto-modified
61
- "tools_sessions.py", "tools_coordination.py", "tools_reminders.py",
62
- "tools_reminders_crud.py", "tools_learnings.py", "tools_credentials.py",
63
- "tools_task_history.py", "tools_menu.py",
64
- }
65
-
66
- # ── Claude CLI path ──────────────────────────────────────────────────────
67
- CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
68
-
69
- # ── Logging ──────────────────────────────────────────────────────────────
70
- LOG_DIR.mkdir(parents=True, exist_ok=True)
71
- LOG_FILE = LOG_DIR / "evolution.log"
72
-
73
-
74
- def log(msg: str):
75
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
76
- line = f"[{ts}] {msg}"
77
- print(line, flush=True)
78
- with open(LOG_FILE, "a") as f:
79
- f.write(line + "\n")
80
-
81
-
82
- # ── Import from evolution_cycle.py (lives in NEXO_CODE, i.e. src/) ──────
83
- sys.path.insert(0, str(NEXO_CODE))
84
- from evolution_cycle import (
85
- load_objective, save_objective, get_week_data, build_evolution_prompt,
86
- dry_run_restore_test, max_auto_changes, create_snapshot
87
- )
88
-
89
-
90
- # ── Consecutive failure tracking ─────────────────────────────────────────
91
- def get_consecutive_failures() -> int:
92
- obj = load_objective()
93
- return obj.get("consecutive_failures", 0)
94
-
95
-
96
- def set_consecutive_failures(count: int):
97
- obj = load_objective()
98
- obj["consecutive_failures"] = count
99
- save_objective(obj)
100
-
101
-
102
- # ── Claude CLI call ──────────────────────────────────────────────────────
103
- CLI_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
104
-
105
-
106
- def verify_claude_cli() -> bool:
107
- def call_claude_cli(prompt: str) -> str:
108
- """Call claude -p prompt --model opus via subprocess. Returns stdout text."""
109
- env = os.environ.copy()
110
- env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
111
- env.pop("CLAUDECODE", None)
112
- env.pop("CLAUDE_CODE", None)
113
-
114
- result = subprocess.run(
115
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
116
- "--output-format", "text",
117
- "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
118
- capture_output=True,
119
- text=True,
120
- timeout=CLI_TIMEOUT,
121
- env=env,
122
- )
123
- if result.returncode != 0:
124
- raise RuntimeError(f"claude CLI exited {result.returncode}: {result.stderr[:500]}")
125
- return result.stdout
126
-
127
-
128
- # ── File safety validation ───────────────────────────────────────────────
129
- def is_safe_path(filepath: str, mode: str = "auto") -> bool:
130
- """Check if a file path is within safe zones and not immutable.
131
- mode='auto' (public): restricted to scripts/ and plugins/ only.
132
- mode='review' (owner): broader zones but nothing executes without approval anyway.
133
- """
134
- expanded = str(Path(filepath).expanduser().resolve())
135
- filename = Path(expanded).name
136
-
137
- if filename in IMMUTABLE_FILES:
138
- return False
139
-
140
- prefixes = AUTO_SAFE_PREFIXES if mode == "review" else AUTO_SAFE_PREFIXES_PUBLIC
141
- for prefix in prefixes:
142
- resolved_prefix = str(Path(prefix).expanduser().resolve())
143
- if expanded.startswith(resolved_prefix):
144
- return True
145
-
146
- return False
147
-
148
-
149
- def validate_syntax(filepath: str) -> tuple[bool, str]:
150
- """Basic syntax validation for known file types."""
151
- path = Path(filepath)
152
- ext = path.suffix
153
-
154
- if ext == ".py":
155
- try:
156
- py_compile.compile(str(path), doraise=True)
157
- return True, "Python syntax OK"
158
- except Exception as e:
159
- return False, f"Validation error: {e}"
160
-
161
- elif ext == ".sh":
162
- try:
163
- result = subprocess.run(
164
- ["bash", "-n", str(path)],
165
- capture_output=True, text=True, timeout=10
166
- )
167
- if result.returncode == 0:
168
- return True, "Bash syntax OK"
169
- return False, f"Bash syntax error: {result.stderr[:200]}"
170
- except Exception as e:
171
- return False, f"Validation error: {e}"
172
-
173
- elif ext == ".json":
174
- try:
175
- json.loads(Path(filepath).read_text())
176
- return True, "JSON valid"
177
- except Exception as e:
178
- return False, f"JSON error: {e}"
179
-
180
- elif ext == ".md":
181
- return True, "Markdown (no validation needed)"
182
-
183
- return True, f"No validator for {ext} (accepted)"
184
-
185
-
186
- # ── Apply a single change operation ──────────────────────────────────────
187
- def apply_change(change: dict) -> tuple[bool, str]:
188
- """Apply a single file change operation. Returns (success, message)."""
189
- filepath = str(Path(change["file"]).expanduser())
190
- operation = change.get("operation", "")
191
- content = change.get("content", "")
192
-
193
- if not is_safe_path(filepath):
194
- return False, f"BLOCKED: {filepath} is outside safe zones or immutable"
195
-
196
- try:
197
- if operation == "create":
198
- if Path(filepath).exists():
199
- return False, f"BLOCKED: {filepath} already exists (create requires new file)"
200
- Path(filepath).parent.mkdir(parents=True, exist_ok=True)
201
- Path(filepath).write_text(content)
202
- # Make scripts executable
203
- if filepath.endswith(".sh") or filepath.endswith(".py"):
204
- os.chmod(filepath, 0o755)
205
- return True, f"Created {filepath}"
206
-
207
- elif operation == "replace":
208
- search = change.get("search", "")
209
- if not search:
210
- return False, "BLOCKED: replace operation requires 'search' field"
211
- if not Path(filepath).exists():
212
- return False, f"BLOCKED: {filepath} does not exist"
213
- original = Path(filepath).read_text()
214
- count = original.count(search)
215
- if count == 0:
216
- return False, f"BLOCKED: search text not found in {filepath}"
217
- if count > 1:
218
- return False, f"BLOCKED: search text matches {count} times (must be unique)"
219
- new_content = original.replace(search, content, 1)
220
- Path(filepath).write_text(new_content)
221
- return True, f"Replaced in {filepath}"
222
-
223
- elif operation == "append":
224
- if not Path(filepath).exists():
225
- return False, f"BLOCKED: {filepath} does not exist"
226
- with open(filepath, "a") as f:
227
- f.write(content)
228
- return True, f"Appended to {filepath}"
229
-
230
- else:
231
- return False, f"BLOCKED: unknown operation '{operation}'"
232
-
233
- except Exception as e:
234
- return False, f"ERROR: {e}"
235
-
236
-
237
- # ── Execute AUTO proposals ───────────────────────────────────────────────
238
- def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connection) -> dict:
239
- """Execute an AUTO proposal with snapshot/apply/validate/rollback."""
240
- changes = proposal.get("changes", [])
241
- if not changes:
242
- return {"status": "skipped", "reason": "No changes array in proposal"}
243
-
244
- # Validate all paths first
245
- for change in changes:
246
- filepath = str(Path(change["file"]).expanduser())
247
- if not is_safe_path(filepath):
248
- return {"status": "blocked", "reason": f"Unsafe path: {filepath}"}
249
-
250
- # Collect files to snapshot (existing files only)
251
- files_to_backup = []
252
- for change in changes:
253
- filepath = str(Path(change["file"]).expanduser())
254
- if Path(filepath).exists():
255
- files_to_backup.append(filepath)
256
-
257
- # Create snapshot
258
- snapshot_ref = None
259
- if files_to_backup:
260
- snapshot_ref = create_snapshot(files_to_backup)
261
- log(f" Snapshot created: {snapshot_ref}")
262
-
263
- # Apply changes
264
- applied_files = []
265
- all_results = []
266
- try:
267
- for change in changes:
268
- success, msg = apply_change(change)
269
- all_results.append(msg)
270
- log(f" {msg}")
271
- if not success:
272
- raise RuntimeError(f"Change failed: {msg}")
273
- filepath = str(Path(change["file"]).expanduser())
274
- applied_files.append(filepath)
275
-
276
- # Validate all modified/created files
277
- for filepath in applied_files:
278
- valid, vmsg = validate_syntax(filepath)
279
- all_results.append(vmsg)
280
- log(f" Validate: {vmsg}")
281
- if not valid:
282
- raise RuntimeError(f"Validation failed: {vmsg}")
283
-
284
- return {
285
- "status": "applied",
286
- "snapshot_ref": snapshot_ref,
287
- "files_changed": applied_files,
288
- "test_result": "; ".join(all_results),
289
- }
290
-
291
- except RuntimeError as e:
292
- # Rollback
293
- log(f" ROLLBACK: {e}")
294
- if snapshot_ref:
295
- try:
296
- restore_script = CLAUDE_DIR / "scripts" / "nexo-snapshot-restore.sh"
297
- subprocess.run(
298
- [str(restore_script), snapshot_ref],
299
- capture_output=True, timeout=15, check=True
300
- )
301
- log(f" Restored from snapshot {snapshot_ref}")
302
- except Exception as re:
303
- log(f" CRITICAL: Restore failed: {re}")
304
- else:
305
- # Remove created files that didn't exist before
306
- for filepath in applied_files:
307
- if filepath not in files_to_backup:
308
- Path(filepath).unlink(missing_ok=True)
309
- log(f" Removed created file: {filepath}")
310
-
311
- return {
312
- "status": "failed",
313
- "snapshot_ref": snapshot_ref,
314
- "files_changed": [],
315
- "test_result": f"ROLLBACK: {e}; " + "; ".join(all_results),
316
- }
317
-
318
-
319
- # ── Review followup for owner mode ──────────────────────────────────────
320
- def _create_review_followup(conn: sqlite3.Connection, cycle_num: int,
321
- items: list[dict], analysis: str):
322
- """Create a followup summarizing Evolution proposals for owner review."""
323
- tomorrow = (date.today() + timedelta(days=1)).isoformat()
324
- followup_id = f"NF-EVO-C{cycle_num}"
325
-
326
- public_items = [i for i in items if i.get("scope") == "public"]
327
- local_items = [i for i in items if i.get("scope") != "public"]
328
-
329
- lines = [f"Evolution Cycle #{cycle_num} — {len(items)} proposals to review."]
330
- lines.append(f"Analysis: {analysis[:200]}")
331
- lines.append("")
332
-
333
- if public_items:
334
- lines.append(f"FOR EVERYONE ({len(public_items)}):")
335
- for i, item in enumerate(public_items, 1):
336
- lines.append(f" {i}. [{item['dimension']}] {item['action'][:120]}")
337
- lines.append(f" Why: {item['reasoning'][:100]}")
338
- lines.append("")
339
-
340
- if local_items:
341
- lines.append(f"FOR YOU ONLY ({len(local_items)}):")
342
- for i, item in enumerate(local_items, 1):
343
- lines.append(f" {i}. [{item['dimension']}] {item['action'][:120]}")
344
- lines.append(f" Why: {item['reasoning'][:100]}")
345
-
346
- description = "\n".join(lines)
347
-
348
- try:
349
- now_epoch = datetime.now().timestamp()
350
- conn.execute(
351
- "INSERT OR REPLACE INTO followups (id, description, date, status, verification, created_at, updated_at) "
352
- "VALUES (?, ?, ?, 'pending', ?, ?, ?)",
353
- (followup_id, description, tomorrow,
354
- f"SELECT * FROM evolution_log WHERE cycle_number={cycle_num}",
355
- now_epoch, now_epoch)
356
- )
357
- conn.commit()
358
- log(f" Followup {followup_id} created for {tomorrow}")
359
- except Exception as e:
360
- log(f" WARN: Failed to create followup: {e}")
361
-
362
-
363
- # ── Main run ─────────────────────────────────────────────────────────────
364
- def run():
365
- log("=" * 60)
366
- log("NEXO Evolution cycle starting (standalone, v2 — real execution)")
367
-
368
- # Check objective
369
- objective = load_objective()
370
- if not objective:
371
- log("ERROR: No evolution-objective.json found")
372
- sys.exit(1)
373
- if not objective.get("evolution_enabled", True):
374
- log(f"Evolution DISABLED: {objective.get('disabled_reason', 'unknown')}")
375
- return
376
-
377
- # Circuit breaker: consecutive failures
378
- failures = get_consecutive_failures()
379
- if failures >= MAX_CONSECUTIVE_FAILURES:
380
- log(f"CIRCUIT BREAKER: {failures} consecutive failures. Disabling evolution.")
381
- objective["evolution_enabled"] = False
382
- objective["disabled_reason"] = f"Circuit breaker: {failures} consecutive failures at {datetime.now().isoformat()}"
383
- save_objective(objective)
384
- return
385
-
386
- # Dry-run restore test
387
- log("Running restore dry-run test...")
388
- if not dry_run_restore_test():
389
- log("CRITICAL: Restore test failed — aborting")
390
- set_consecutive_failures(failures + 1)
391
- sys.exit(1)
392
- log("Restore test PASSED")
393
-
394
- # Gather data
395
- log("Gathering week data from nexo.db...")
396
- week_data = get_week_data(str(NEXO_DB))
397
- log(f" Learnings: {len(week_data.get('learnings', []))}")
398
- log(f" Decisions: {len(week_data.get('decisions', []))}")
399
- log(f" Changes: {len(week_data.get('changes', []))}")
400
- log(f" Diaries: {len(week_data.get('diaries', []))}")
401
-
402
- # Build prompt
403
- prompt = build_evolution_prompt(week_data, objective)
404
- log(f"Prompt built: {len(prompt)} chars")
405
-
406
- # Verify Claude CLI is authenticated before calling
407
- if not verify_claude_cli():
408
- log("Claude CLI not available or not authenticated. Skipping evolution run.")
409
- return
410
-
411
- # Call Opus via claude -p
412
- log("Calling claude -p --model opus...")
413
- try:
414
- raw_response = call_claude_cli(prompt)
415
- except Exception as e:
416
- log(f"claude CLI call failed: {e}")
417
- set_consecutive_failures(failures + 1)
418
- return
419
-
420
- log(f"Response received: {len(raw_response)} chars")
421
-
422
- # Parse JSON
423
- try:
424
- text = raw_response
425
- if "```json" in text:
426
- text = text.split("```json")[1].split("```")[0]
427
- elif "```" in text:
428
- text = text.split("```")[1].split("```")[0]
429
- response = json.loads(text.strip())
430
- except Exception as e:
431
- log(f"JSON parse failed: {e}")
432
- log(f"Raw (first 500): {raw_response[:500]}")
433
- set_consecutive_failures(failures + 1)
434
- return
435
-
436
- # Reset consecutive failures on successful parse
437
- set_consecutive_failures(0)
438
-
439
- log(f"Analysis: {response.get('analysis', 'N/A')[:200]}")
440
-
441
- # Log patterns
442
- for p in response.get("patterns", []):
443
- log(f" Pattern [{p.get('type', '?')}]: {p.get('description', '')[:100]} (freq: {p.get('frequency', '?')})")
444
-
445
- # Process proposals
446
- proposals = response.get("proposals", [])
447
- cycle_num = objective.get("total_evolutions", 0) + 1
448
- max_auto = max_auto_changes(objective.get("total_evolutions", 0))
449
- auto_count = 0
450
- auto_applied = 0
451
- evolution_mode = objective.get("evolution_mode", "auto") # "auto" (public) or "review" (owner)
452
-
453
- conn = sqlite3.connect(str(NEXO_DB), timeout=10)
454
- conn.execute("PRAGMA busy_timeout=5000")
455
-
456
- # In "review" mode: log everything as pending_review, create followup
457
- # In "auto" mode: execute AUTO proposals, log PROPOSE as proposed
458
- review_items = []
459
-
460
- for p in proposals:
461
- classification = p.get("classification", "propose")
462
- dimension = p.get("dimension", "other")
463
- action = p.get("action", "")
464
- reasoning = p.get("reasoning", "")
465
- scope = p.get("scope", "local") # "public" or "local"
466
-
467
- if evolution_mode == "review":
468
- # Owner mode: nothing executes, everything queued for review
469
- log(f" QUEUED [{scope}]: {action[:80]}")
470
- conn.execute(
471
- "INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
472
- "reasoning, status) VALUES (?, ?, ?, ?, ?, ?)",
473
- (cycle_num, dimension, action, classification, reasoning, "pending_review")
474
- )
475
- review_items.append({
476
- "dimension": dimension,
477
- "action": action,
478
- "reasoning": reasoning,
479
- "scope": scope,
480
- "classification": classification,
481
- })
482
-
483
- elif classification == "auto" and auto_count < max_auto:
484
- # Public mode: execute AUTO proposals
485
- auto_count += 1
486
- log(f" AUTO #{auto_count}/{max_auto}: {action[:80]}")
487
-
488
- result = execute_auto_proposal(p, cycle_num, conn)
489
- status = result["status"]
490
-
491
- conn.execute(
492
- "INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
493
- "reasoning, status, files_changed, snapshot_ref, test_result) "
494
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
495
- (cycle_num, dimension, action, "auto", reasoning, status,
496
- json.dumps(result.get("files_changed", [])),
497
- result.get("snapshot_ref", ""),
498
- result.get("test_result", ""))
499
- )
500
-
501
- if status == "applied":
502
- auto_applied += 1
503
- log(f" APPLIED successfully")
504
- elif status == "blocked":
505
- log(f" BLOCKED: {result.get('test_result', '')}")
506
- elif status == "skipped":
507
- log(f" SKIPPED: {result.get('reason', '')}")
508
- else:
509
- log(f" FAILED: {result.get('test_result', '')[:100]}")
510
-
511
- else:
512
- # PROPOSE or over auto limit
513
- if classification == "auto" and auto_count >= max_auto:
514
- log(f" AUTO→PROPOSE (over limit {max_auto}): {action[:80]}")
515
- classification = "propose"
516
- else:
517
- log(f" PROPOSE: {action[:80]}")
518
-
519
- conn.execute(
520
- "INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
521
- "reasoning, status) VALUES (?, ?, ?, ?, ?, ?)",
522
- (cycle_num, dimension, action, classification, reasoning, "proposed")
523
- )
524
-
525
- conn.commit()
526
-
527
- # In review mode: create followup for owner
528
- if evolution_mode == "review" and review_items:
529
- _create_review_followup(conn, cycle_num, review_items, response.get("analysis", ""))
530
-
531
- # Update metrics
532
- scores = response.get("dimension_scores", {})
533
- evidence = response.get("score_evidence", {})
534
- current = week_data.get("current_metrics", {})
535
-
536
- for dim, score in scores.items():
537
- if isinstance(score, (int, float)) and 0 <= score <= 100:
538
- prev = current.get(dim, {}).get("score", 0)
539
- delta = int(score) - prev
540
- conn.execute(
541
- "INSERT INTO evolution_metrics (dimension, score, evidence, delta) VALUES (?, ?, ?, ?)",
542
- (dim, int(score), json.dumps(evidence.get(dim, "")), delta)
543
- )
544
-
545
- conn.commit()
546
- conn.close()
547
-
548
- # Update objective
549
- objective["last_evolution"] = str(date.today())
550
- objective["total_evolutions"] = cycle_num
551
- objective["total_proposals_made"] = objective.get("total_proposals_made", 0) + len(proposals)
552
- objective["total_auto_applied"] = objective.get("total_auto_applied", 0) + auto_applied
553
- for dim, score in scores.items():
554
- if dim in objective.get("dimensions", {}) and isinstance(score, (int, float)):
555
- objective["dimensions"][dim]["current"] = int(score)
556
-
557
- objective.setdefault("history", []).insert(0, {
558
- "cycle": cycle_num,
559
- "date": str(date.today()),
560
- "proposals": len(proposals),
561
- "auto_count": auto_count,
562
- "auto_applied": auto_applied,
563
- "analysis": response.get("analysis", "")[:200]
564
- })
565
- objective["history"] = objective["history"][:12]
566
-
567
- save_objective(objective)
568
-
569
- log(f"Evolution cycle #{cycle_num} COMPLETE: {len(proposals)} proposals "
570
- f"({auto_count} auto, {auto_applied} applied, "
571
- f"{len(proposals) - auto_count} propose)")
572
- log("=" * 60)
573
-
574
-
575
- def _update_catchup_state():
576
- """Register successful run for catch-up."""
577
- try:
578
- import json as _json
579
- from pathlib import Path as _Path
580
-
581
- _state_file = NEXO_HOME / "operations" / ".catchup-state.json"
582
- _state = _json.loads(_state_file.read_text()) if _state_file.exists() else {}
583
- _state["evolution"] = datetime.now().isoformat()
584
- _state_file.write_text(_json.dumps(_state, indent=2))
585
- except Exception:
586
- pass
587
-
588
-
589
- if __name__ == "__main__":
590
- try:
591
- run()
592
- _update_catchup_state()
593
- except Exception as e:
594
- log(f"FATAL: {e}")
595
- import traceback
596
- log(traceback.format_exc())
597
- sys.exit(1)