nexo-brain 5.3.20 → 5.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/auto_update.py +11 -8
  4. package/src/dashboard/static/favicon 2.svg +32 -0
  5. package/src/dashboard/static/nexo-logo 2.png +0 -0
  6. package/src/dashboard/static/nexo-logo 2.svg +40 -0
  7. package/src/dashboard/static/style 2.css +2458 -0
  8. package/src/dashboard/templates/adaptive 2.html +118 -0
  9. package/src/dashboard/templates/artifacts 2.html +133 -0
  10. package/src/dashboard/templates/backups 2.html +136 -0
  11. package/src/dashboard/templates/base 2.html +417 -0
  12. package/src/dashboard/templates/calendar 2.html +591 -0
  13. package/src/dashboard/templates/chat 2.html +356 -0
  14. package/src/dashboard/templates/claims 2.html +259 -0
  15. package/src/dashboard/templates/cortex 2.html +321 -0
  16. package/src/dashboard/templates/credentials 2.html +128 -0
  17. package/src/dashboard/templates/crons 2.html +370 -0
  18. package/src/dashboard/templates/dashboard 2.html +494 -0
  19. package/src/dashboard/templates/dreams 2.html +252 -0
  20. package/src/dashboard/templates/email 2.html +160 -0
  21. package/src/dashboard/templates/evolution 2.html +189 -0
  22. package/src/dashboard/templates/feed 2.html +249 -0
  23. package/src/dashboard/templates/followup_health 2.html +170 -0
  24. package/src/dashboard/templates/graph 2.html +201 -0
  25. package/src/dashboard/templates/guard 2.html +259 -0
  26. package/src/dashboard/templates/inbox 2.html +251 -0
  27. package/src/dashboard/templates/memory 2.html +420 -0
  28. package/src/dashboard/templates/operations 2.html +608 -0
  29. package/src/dashboard/templates/plugins 2.html +185 -0
  30. package/src/dashboard/templates/protocol 2.html +199 -0
  31. package/src/dashboard/templates/rules 2.html +246 -0
  32. package/src/dashboard/templates/sentiment 2.html +247 -0
  33. package/src/dashboard/templates/sessions 2.html +218 -0
  34. package/src/dashboard/templates/skills 2.html +329 -0
  35. package/src/dashboard/templates/somatic 2.html +73 -0
  36. package/src/dashboard/templates/triggers 2.html +133 -0
  37. package/src/dashboard/templates/trust 2.html +360 -0
  38. package/src/db/__init__ 2.py +259 -0
  39. package/src/db/_core 2.py +437 -0
  40. package/src/db/_credentials 2.py +124 -0
  41. package/src/db/_episodic 2.py +762 -0
  42. package/src/db/_evolution 2.py +54 -0
  43. package/src/db/_fts 2.py +406 -0
  44. package/src/db/_goal_profiles 2.py +376 -0
  45. package/src/db/_hot_context 2.py +660 -0
  46. package/src/db/_outcomes 2.py +800 -0
  47. package/src/db/_personal_scripts 2.py +582 -0
  48. package/src/db/_sessions 2.py +330 -0
  49. package/src/db/_tasks 2.py +91 -0
  50. package/src/db/_watchers 2.py +173 -0
  51. package/src/doctor/formatters 2.py +52 -0
  52. package/src/doctor/models 2.py +69 -0
  53. package/src/doctor/planes 2.py +87 -0
  54. package/src/doctor/providers/__init__ 2.py +1 -0
  55. package/src/doctor/providers/deep 2.py +367 -0
  56. package/src/evolution_cycle 2.py +519 -0
  57. package/src/hooks/auto_capture 2.py +208 -0
  58. package/src/hooks/caffeinate-guard 2.sh +8 -0
  59. package/src/hooks/capture-session 2.sh +21 -0
  60. package/src/hooks/capture-tool-logs 2.sh +158 -0
  61. package/src/hooks/daily-briefing-check 2.sh +33 -0
  62. package/src/hooks/heartbeat-enforcement 2.py +90 -0
  63. package/src/hooks/heartbeat-posttool 2.sh +18 -0
  64. package/src/hooks/inbox-hook 2.sh +76 -0
  65. package/src/hooks/post-compact 2.sh +152 -0
  66. package/src/hooks/pre-compact 2.sh +169 -0
  67. package/src/hooks/protocol-guardrail 2.sh +10 -0
  68. package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
  69. package/src/hooks/session-stop 2.sh +52 -0
  70. package/src/kg_populate 2.py +292 -0
  71. package/src/maintenance 2.py +53 -0
  72. package/src/memory_backends 2.py +71 -0
  73. package/src/migrate_embeddings 2.py +124 -0
  74. package/src/nexo_sdk 2.py +103 -0
  75. package/src/observability 2.py +199 -0
  76. package/src/plugin_loader 2.py +217 -0
  77. package/src/plugins/__init__ 2.py +0 -0
  78. package/src/plugins/artifact_registry 2.py +450 -0
  79. package/src/plugins/backup 2.py +127 -0
  80. package/src/plugins/claims_tools 2.py +119 -0
  81. package/src/plugins/cognitive_memory 2.py +609 -0
  82. package/src/plugins/core_rules 2.py +252 -0
  83. package/src/plugins/cortex 2.py +1155 -0
  84. package/src/plugins/entities 2.py +67 -0
  85. package/src/plugins/episodic_memory 2.py +560 -0
  86. package/src/plugins/evolution 2.py +167 -0
  87. package/src/plugins/goal_engine 2.py +142 -0
  88. package/src/plugins/guard 2.py +862 -0
  89. package/src/plugins/impact 2.py +29 -0
  90. package/src/plugins/knowledge_graph_tools 2.py +137 -0
  91. package/src/plugins/media_memory_tools 2.py +98 -0
  92. package/src/plugins/memory_export 2.py +196 -0
  93. package/src/plugins/outcomes 2.py +130 -0
  94. package/src/plugins/personal_scripts 2.py +117 -0
  95. package/src/plugins/preferences 2.py +47 -0
  96. package/src/plugins/protocol 2.py +1449 -0
  97. package/src/plugins/simple_api 2.py +106 -0
  98. package/src/plugins/skills 2.py +341 -0
  99. package/src/plugins/state_watchers 2.py +79 -0
  100. package/src/plugins/update 2.py +986 -0
  101. package/src/plugins/user_state_tools 2.py +43 -0
  102. package/src/plugins/workflow 2.py +588 -0
  103. package/src/protocol_settings 2.py +59 -0
  104. package/src/public_contribution 2.py +466 -0
  105. package/src/public_evolution_queue 2.py +241 -0
  106. package/src/requirements 2.txt +14 -0
  107. package/src/retroactive_learnings 2.py +373 -0
  108. package/src/rules/__init__ 2.py +0 -0
  109. package/src/rules/core-rules 2.json +331 -0
  110. package/src/rules/migrate 2.py +207 -0
  111. package/src/runtime_power 2.py +874 -0
  112. package/src/script_registry 2.py +1559 -0
  113. package/src/scripts/check-context 2.py +272 -0
  114. package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
  115. package/src/scripts/deep-sleep/collect 2.py +928 -0
  116. package/src/scripts/deep-sleep/extract 2.py +330 -0
  117. package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
  118. package/src/scripts/deep-sleep/synthesize 2.py +312 -0
  119. package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
  120. package/src/scripts/nexo-agent-run 2.py +75 -0
  121. package/src/scripts/nexo-auto-update 2.py +6 -0
  122. package/src/scripts/nexo-backup 2.sh +25 -0
  123. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  124. package/src/scripts/nexo-catchup 2.py +300 -0
  125. package/src/scripts/nexo-cognitive-decay 2.py +257 -0
  126. package/src/scripts/nexo-cortex-cycle 2.py +293 -0
  127. package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
  128. package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
  129. package/src/scripts/nexo-dashboard 2.sh +29 -0
  130. package/src/scripts/nexo-deep-sleep 2.sh +86 -0
  131. package/src/scripts/nexo-evolution-run 2.py +1664 -0
  132. package/src/scripts/nexo-followup-hygiene 2.py +139 -0
  133. package/src/scripts/nexo-hook-record 2.py +42 -0
  134. package/src/scripts/nexo-immune 2.py +936 -0
  135. package/src/scripts/nexo-impact-scorer 2.py +117 -0
  136. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  137. package/src/scripts/nexo-install 2.py +6 -0
  138. package/src/scripts/nexo-learning-housekeep 2.py +401 -0
  139. package/src/scripts/nexo-learning-validator 2.py +266 -0
  140. package/src/scripts/nexo-migrate 2.py +260 -0
  141. package/src/scripts/nexo-outcome-checker 2.py +127 -0
  142. package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
  143. package/src/scripts/nexo-pre-commit 2.py +120 -0
  144. package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
  145. package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
  146. package/src/scripts/nexo-reflection 2.py +256 -0
  147. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  148. package/src/scripts/nexo-sleep 2.py +631 -0
  149. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  150. package/src/scripts/nexo-sync-clients 2.py +16 -0
  151. package/src/scripts/nexo-synthesis 2.py +475 -0
  152. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  153. package/src/scripts/nexo-update 2.sh +306 -0
  154. package/src/scripts/nexo-watchdog 2.sh +1207 -0
  155. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  156. package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
  157. package/src/server 2.py +1296 -0
  158. package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
  159. package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
  160. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
  161. package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
  162. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
  163. package/src/skills/run-release-final-audit/guide 2.md +16 -0
  164. package/src/skills/run-release-final-audit/script 2.py +259 -0
  165. package/src/skills/run-release-final-audit/skill 2.json +77 -0
  166. package/src/skills/run-runtime-doctor/guide 2.md +12 -0
  167. package/src/skills/run-runtime-doctor/script 2.py +21 -0
  168. package/src/skills/run-runtime-doctor/skill 2.json +25 -0
  169. package/src/skills_runtime 2.py +932 -0
  170. package/src/state_watchers_runtime 2.py +475 -0
  171. package/src/storage_router 2.py +32 -0
  172. package/src/system_catalog 2.py +786 -0
  173. package/src/tools_coordination 2.py +103 -0
  174. package/src/tools_credentials 2.py +68 -0
  175. package/src/tools_drive 2.py +487 -0
  176. package/src/tools_hot_context 2.py +163 -0
  177. package/src/tools_learnings 2.py +612 -0
  178. package/src/tools_menu 2.py +229 -0
  179. package/src/tools_reminders 2.py +88 -0
  180. package/src/tools_reminders_crud 2.py +363 -0
  181. package/src/tools_sessions 2.py +1054 -0
  182. package/src/tools_system_catalog 2.py +19 -0
  183. package/src/tools_task_history 2.py +57 -0
  184. package/src/tools_transcripts 2.py +98 -0
  185. package/src/transcript_utils 2.py +412 -0
  186. package/src/user_context 2.py +46 -0
  187. package/src/user_data_portability 2.py +328 -0
  188. package/src/user_state_model 2.py +170 -0
  189. package/templates/CLAUDE.md 2.template +108 -0
  190. package/templates/CODEX.AGENTS.md 2.template +66 -0
  191. package/templates/launchagents/README 2.md +132 -0
  192. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
  193. package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
  194. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
  195. package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
  196. package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
  197. package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
  198. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
  199. package/templates/launchagents/com.nexo.immune 2.plist +41 -0
  200. package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
  201. package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
  202. package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
  203. package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
  204. package/templates/nexo_helper 2.py +301 -0
  205. package/templates/openclaw 2.json +13 -0
  206. package/templates/plugin-template 2.py +40 -0
  207. package/templates/script-template 2.py +59 -0
  208. package/templates/script-template 2.sh +13 -0
  209. package/templates/skill-script-template 2.py +48 -0
  210. package/templates/skill-template 2.md +33 -0
@@ -0,0 +1,519 @@
1
+ """NEXO Evolution Cycle — Self-improvement via Opus API.
2
+
3
+ Runs weekly after DMN. Analyzes patterns, proposes improvements.
4
+ v1: observe-only (all proposals logged as 'proposed' for the user to review).
5
+ v1.1 (future): sandbox execution of auto-approved changes.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import shutil
11
+ import subprocess
12
+ import sqlite3
13
+ import time
14
+ from datetime import datetime, date, timedelta
15
+ from pathlib import Path
16
+
17
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
18
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(NEXO_HOME)))
19
+ NEXO_DB = NEXO_HOME / "data" / "nexo.db"
20
+ SANDBOX_DIR = NEXO_HOME / "sandbox" / "workspace"
21
+ SNAPSHOTS_DIR = NEXO_HOME / "snapshots"
22
+ RESTORE_LOG = NEXO_HOME / "logs" / "snapshot-restores.log"
23
+
24
+ # Evolution config: brain/ (canonical) > cortex/ (legacy) > NEXO_CODE (dev)
25
+ def _resolve_evolution_file(name: str) -> Path:
26
+ for candidate in [NEXO_HOME / "brain" / name, NEXO_HOME / "cortex" / name, NEXO_CODE / name]:
27
+ if candidate.exists():
28
+ return candidate
29
+ return NEXO_HOME / "brain" / name # default canonical path
30
+
31
+ OBJECTIVE_FILE = _resolve_evolution_file("evolution-objective.json")
32
+ PROMPT_FILE = _resolve_evolution_file("evolution-prompt.md")
33
+
34
+ MAX_SNAPSHOTS = 8
35
+
36
+
37
+ def _normalize_dimensions(raw: dict | None) -> dict:
38
+ normalized = {}
39
+ for key, value in (raw or {}).items():
40
+ canonical_key = "agi" if key == "agi_readiness" else key
41
+ if isinstance(value, dict):
42
+ normalized[canonical_key] = {
43
+ "current": int(value.get("current", 0) or 0),
44
+ "target": int(value.get("target", 0) or 0),
45
+ }
46
+ else:
47
+ normalized[canonical_key] = {
48
+ "current": 0,
49
+ "target": int(value or 0),
50
+ }
51
+ return normalized
52
+
53
+
54
+ def normalize_objective(obj: dict | None) -> dict:
55
+ """Upgrade legacy objective files to the canonical schema."""
56
+ source = dict(obj or {})
57
+
58
+ if "evolution_mode" in source:
59
+ mode = str(source.get("evolution_mode") or "auto").strip().lower()
60
+ if mode in {"public", "public_core", "contributor", "draft_prs"}:
61
+ mode = "public_core"
62
+ else:
63
+ legacy_mode = str(source.get("review_mode") or "").strip().lower()
64
+ if legacy_mode in {"manual", "review"}:
65
+ mode = "review"
66
+ elif legacy_mode in {"managed", "hybrid", "owner", "core"}:
67
+ mode = "managed"
68
+ elif legacy_mode in {"public", "public_core", "contributor", "draft_prs"}:
69
+ mode = "public_core"
70
+ else:
71
+ mode = "auto"
72
+
73
+ if mode not in {"auto", "review", "managed", "public_core"}:
74
+ mode = "auto"
75
+
76
+ dimensions = source.get("dimensions")
77
+ if not isinstance(dimensions, dict) or not dimensions:
78
+ dimensions = _normalize_dimensions(source.get("dimension_targets"))
79
+ else:
80
+ dimensions = _normalize_dimensions(dimensions)
81
+
82
+ defaults = {
83
+ "episodic_memory": {"current": 0, "target": 90},
84
+ "autonomy": {"current": 0, "target": 80},
85
+ "proactivity": {"current": 0, "target": 70},
86
+ "self_improvement": {"current": 0, "target": 60},
87
+ "agi": {"current": 0, "target": 20},
88
+ }
89
+ merged_dimensions = dict(defaults)
90
+ merged_dimensions.update(dimensions)
91
+
92
+ normalized = dict(source)
93
+ normalized["evolution_mode"] = mode
94
+ normalized["dimensions"] = merged_dimensions
95
+ normalized["total_evolutions"] = int(source.get("total_evolutions", source.get("cycles_completed", 0)) or 0)
96
+ normalized["last_evolution"] = source.get("last_evolution", source.get("last_cycle"))
97
+ normalized["total_proposals_made"] = int(source.get("total_proposals_made", 0) or 0)
98
+ normalized["total_auto_applied"] = int(source.get("total_auto_applied", 0) or 0)
99
+ normalized["consecutive_failures"] = int(source.get("consecutive_failures", 0) or 0)
100
+ normalized["history"] = source.get("history", []) if isinstance(source.get("history"), list) else []
101
+ normalized["evolution_enabled"] = bool(source.get("evolution_enabled", True))
102
+ normalized.pop("review_mode", None)
103
+ normalized.pop("dimension_targets", None)
104
+ normalized.pop("cycles_completed", None)
105
+ normalized.pop("last_cycle", None)
106
+ return normalized
107
+
108
+
109
+ def load_objective() -> dict:
110
+ if OBJECTIVE_FILE.exists():
111
+ return normalize_objective(json.loads(OBJECTIVE_FILE.read_text()))
112
+ return normalize_objective({})
113
+
114
+
115
+ def save_objective(obj: dict):
116
+ OBJECTIVE_FILE.parent.mkdir(parents=True, exist_ok=True)
117
+ OBJECTIVE_FILE.write_text(json.dumps(normalize_objective(obj), indent=2, ensure_ascii=False))
118
+
119
+
120
+ def get_week_data(db_path: str) -> dict:
121
+ """Gather last 7 days of learnings, decisions, changes, diaries."""
122
+ conn = sqlite3.connect(db_path, timeout=10)
123
+ try:
124
+ conn.row_factory = sqlite3.Row
125
+ cutoff_epoch = time.time() - 7 * 86400
126
+ cutoff_date = (date.today() - timedelta(days=7)).isoformat()
127
+
128
+ data = {}
129
+
130
+ rows = conn.execute(
131
+ "SELECT category, title, content FROM learnings WHERE created_at > ? ORDER BY created_at DESC LIMIT 50",
132
+ (cutoff_epoch,)
133
+ ).fetchall()
134
+ data["learnings"] = [dict(r) for r in rows]
135
+
136
+ rows = conn.execute(
137
+ "SELECT domain, decision, alternatives, based_on, confidence, outcome FROM decisions "
138
+ "WHERE created_at > ? ORDER BY created_at DESC LIMIT 20",
139
+ (cutoff_date,)
140
+ ).fetchall()
141
+ data["decisions"] = [dict(r) for r in rows]
142
+
143
+ rows = conn.execute(
144
+ "SELECT files, what_changed, why, affects, risks FROM change_log "
145
+ "WHERE created_at > ? ORDER BY created_at DESC LIMIT 30",
146
+ (cutoff_date,)
147
+ ).fetchall()
148
+ data["changes"] = [dict(r) for r in rows]
149
+
150
+ rows = conn.execute(
151
+ "SELECT summary, decisions as diary_decisions, pending, mental_state, domain, user_signals "
152
+ "FROM session_diary WHERE created_at > ? ORDER BY created_at DESC LIMIT 20",
153
+ (cutoff_date,)
154
+ ).fetchall()
155
+ data["diaries"] = [dict(r) for r in rows]
156
+
157
+ rows = conn.execute(
158
+ "SELECT * FROM evolution_log ORDER BY id DESC LIMIT 20"
159
+ ).fetchall()
160
+ data["evolution_history"] = [dict(r) for r in rows]
161
+
162
+ rows = conn.execute(
163
+ "SELECT dimension, score, delta, measured_at FROM evolution_metrics "
164
+ "WHERE id IN (SELECT MAX(id) FROM evolution_metrics GROUP BY dimension)"
165
+ ).fetchall()
166
+ data["current_metrics"] = {r["dimension"]: dict(r) for r in rows}
167
+
168
+ return data
169
+ finally:
170
+ conn.close()
171
+
172
+
173
+ def create_snapshot(files_to_backup: list) -> str:
174
+ """Create a snapshot of specific files before modification."""
175
+ ts = datetime.now().strftime("%Y-%m-%dT%H:%M")
176
+ snap_dir = SNAPSHOTS_DIR / ts
177
+ files_dir = snap_dir / "files"
178
+
179
+ manifest = {
180
+ "created_at": datetime.now().isoformat(),
181
+ "files": [],
182
+ "reason": "evolution_cycle"
183
+ }
184
+
185
+ for filepath in files_to_backup:
186
+ fp = Path(filepath).expanduser()
187
+ if fp.exists():
188
+ rel = str(fp).replace(str(Path.home()) + "/", "")
189
+ dest = files_dir / rel
190
+ dest.parent.mkdir(parents=True, exist_ok=True)
191
+ if os.path.abspath(str(fp)) == os.path.abspath(str(dest)):
192
+ continue # Skip: source and destination are the same file
193
+ shutil.copy2(fp, dest)
194
+ manifest["files"].append(rel)
195
+
196
+ snap_dir.mkdir(parents=True, exist_ok=True)
197
+ (snap_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))
198
+
199
+ latest = SNAPSHOTS_DIR / "latest"
200
+ if latest.is_symlink():
201
+ latest.unlink()
202
+ latest.symlink_to(snap_dir)
203
+
204
+ _cleanup_snapshots()
205
+ return str(snap_dir)
206
+
207
+
208
+ def _cleanup_snapshots():
209
+ """Remove old snapshots, keeping MAX_SNAPSHOTS most recent + golden."""
210
+ if not SNAPSHOTS_DIR.exists():
211
+ return
212
+ snaps = sorted(
213
+ [d for d in SNAPSHOTS_DIR.iterdir()
214
+ if d.is_dir() and d.name not in ("latest", "golden")],
215
+ key=lambda d: d.stat().st_mtime,
216
+ reverse=True
217
+ )
218
+ for old in snaps[MAX_SNAPSHOTS:]:
219
+ shutil.rmtree(old)
220
+
221
+
222
+ def dry_run_restore_test() -> bool:
223
+ """Test that snapshot+restore works before making real changes."""
224
+ test_file = SANDBOX_DIR / "restore-test.txt"
225
+ test_file.parent.mkdir(parents=True, exist_ok=True)
226
+ test_file.write_text("original_content")
227
+
228
+ snap_dir = create_snapshot([str(test_file)])
229
+
230
+ test_file.write_text("modified_content")
231
+
232
+ # Find restore script: NEXO_CODE/scripts/ first, then NEXO_HOME/scripts/
233
+ _nexo_code = Path(os.environ.get("NEXO_CODE", ""))
234
+ restore_script = None
235
+ for candidate in [_nexo_code / "scripts" / "nexo-snapshot-restore.sh",
236
+ NEXO_HOME / "scripts" / "nexo-snapshot-restore.sh"]:
237
+ if candidate.exists():
238
+ restore_script = candidate
239
+ break
240
+ if not restore_script:
241
+ test_file.unlink(missing_ok=True)
242
+ return False # No restore script available
243
+
244
+ try:
245
+ subprocess.run(
246
+ [str(restore_script), snap_dir],
247
+ capture_output=True, timeout=10, check=True
248
+ )
249
+ content = test_file.read_text()
250
+ test_file.unlink(missing_ok=True)
251
+ # Clean up test snapshot
252
+ snap_path = Path(snap_dir)
253
+ if snap_path.exists():
254
+ shutil.rmtree(snap_path)
255
+ return content == "original_content"
256
+ except Exception:
257
+ test_file.unlink(missing_ok=True)
258
+ return False
259
+
260
+
261
+ def build_evolution_prompt(week_data: dict, objective: dict) -> str:
262
+ """Build a SHORT prompt — CLI investigates on its own using tools."""
263
+
264
+ objective_dims = normalize_objective(objective).get("dimensions", {})
265
+ current_scores = {
266
+ dim: int(m["score"])
267
+ for dim, m in week_data.get("current_metrics", {}).items()
268
+ if isinstance(m, dict) and isinstance(m.get("score"), (int, float))
269
+ }
270
+ if not current_scores:
271
+ current_scores = {
272
+ dim: int((payload or {}).get("current", 0) or 0)
273
+ for dim, payload in objective_dims.items()
274
+ if isinstance(payload, dict)
275
+ }
276
+
277
+ # Summary stats only — CLI will dig deeper with tools
278
+ stats = {
279
+ "learnings_this_week": len(week_data.get("learnings", [])),
280
+ "decisions_this_week": len(week_data.get("decisions", [])),
281
+ "changes_this_week": len(week_data.get("changes", [])),
282
+ "diaries_this_week": len(week_data.get("diaries", [])),
283
+ "evolution_history": len(week_data.get("evolution_history", [])),
284
+ "current_scores": current_scores,
285
+ }
286
+
287
+ mode = normalize_objective(objective).get("evolution_mode", "auto")
288
+ total = objective.get("total_evolutions", 0)
289
+ max_auto = max_auto_changes(total)
290
+ if mode == "review":
291
+ mode_desc = "review-only, nothing executes automatically"
292
+ safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/"
293
+ immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, cognitive.py, knowledge_graph.py, tools_*.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md"
294
+ elif mode == "managed":
295
+ mode_desc = f"owner-managed, max {max_auto} auto-applied changes with rollback and followups"
296
+ safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/, NEXO_CODE/src, repo bin/docs/templates/tests"
297
+ immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md, personality.md, user-profile.md"
298
+ elif mode == "public_core":
299
+ mode_desc = "public core contribution via isolated checkout and Draft PR"
300
+ safe_zones = "isolated public repo checkout only"
301
+ immutable_files = "personal runtime, ~/.nexo/**, local DBs/logs, CLAUDE.md, AGENTS.md, user-profile.md"
302
+ else:
303
+ mode_desc = f"public auto, max {max_auto} auto-applied changes in personal safe zones"
304
+ safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/"
305
+ immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, cognitive.py, knowledge_graph.py, tools_*.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md"
306
+
307
+ prompt = f"""You are NEXO Evolution — the weekly self-improvement cycle.
308
+
309
+ YOUR JOB: Analyze the past week and propose concrete improvements to NEXO's codebase.
310
+
311
+ WEEK SUMMARY:
312
+ - {stats['learnings_this_week']} new learnings
313
+ - {stats['decisions_this_week']} decisions made
314
+ - {stats['changes_this_week']} code changes deployed
315
+ - {stats['diaries_this_week']} session diaries
316
+ - {stats['evolution_history']} past evolution proposals
317
+ - Current scores: {json.dumps(stats['current_scores'])}
318
+
319
+ MODE: {mode} ({mode_desc})
320
+ CYCLE: #{total + 1}
321
+
322
+ INVESTIGATE using these tools:
323
+ 1. Bash: sqlite3 {NEXO_DB} "SELECT category, title FROM learnings WHERE created_at > {time.time() - 7*86400} ORDER BY created_at DESC LIMIT 30"
324
+ 2. Bash: sqlite3 {NEXO_DB} "SELECT area, COUNT(*) as cnt FROM error_repetitions GROUP BY area ORDER BY cnt DESC LIMIT 10"
325
+ 3. Read ~/.nexo/coordination/daily-synthesis.md — today's context
326
+ 4. Read ~/.nexo/coordination/postmortem-daily.md — self-critique patterns
327
+ 5. Read ~/.nexo/logs/self-audit-summary.json — system health
328
+ 6. Glob ~/.nexo/scripts/*.py — existing scripts
329
+ 7. Glob ~/.nexo/plugins/*.py — existing plugins
330
+
331
+ LOOK FOR:
332
+ - Repeated errors that guard isn't preventing
333
+ - Scripts or processes that are failing or underperforming
334
+ - Missing functionality that session diaries keep asking for
335
+ - Redundant code or config that could be simplified
336
+ - Patterns in self-critique that suggest systemic issues
337
+
338
+ SAFETY:
339
+ - Safe zones for this mode: {safe_zones}
340
+ - IMMUTABLE files (never touch in this mode): {immutable_files}
341
+ - Every change needs: what file, what to change, why, risk, how to verify
342
+ - AUTO changes must be deterministic. If the edit is ambiguous, risky, or needs human taste, mark it as "propose".
343
+ - In managed mode, failed AUTO changes will be rolled back automatically and turned into followups with evidence.
344
+
345
+ OUTPUT FORMAT (JSON):
346
+ {{
347
+ "analysis": "one paragraph summary of what you found",
348
+ "dimension_scores": {{
349
+ "episodic_memory": 0,
350
+ "autonomy": 0,
351
+ "proactivity": 0,
352
+ "self_improvement": 0,
353
+ "agi": 0
354
+ }},
355
+ "score_evidence": {{
356
+ "episodic_memory": "why this score changed or stayed flat",
357
+ "autonomy": "why this score changed or stayed flat",
358
+ "proactivity": "why this score changed or stayed flat",
359
+ "self_improvement": "why this score changed or stayed flat",
360
+ "agi": "why this score changed or stayed flat"
361
+ }},
362
+ "patterns": [{{"type": "...", "description": "...", "frequency": "..."}}],
363
+ "proposals": [
364
+ {{
365
+ "classification": "auto" or "propose",
366
+ "dimension": "reliability|proactivity|efficiency|safety|learning",
367
+ "action": "what to do",
368
+ "reasoning": "why",
369
+ "scope": "local",
370
+ "changes": [{{"file": "path", "operation": "create|replace|append", "search": "text to find", "content": "new text"}}]
371
+ }}
372
+ ]
373
+ }}
374
+
375
+ Always include all five canonical keys in `dimension_scores` and `score_evidence`.
376
+ Scores must be integers in the 0-100 range and reflect the current week, not targets.
377
+ Max 3 proposals. Quality over quantity. If nothing needs improving, say so."""
378
+
379
+ return prompt
380
+
381
+
382
+ def build_public_contribution_prompt(
383
+ *,
384
+ repo_root: str,
385
+ cycle_number: int,
386
+ queued_candidate: dict | None = None,
387
+ ) -> str:
388
+ """Prompt for the public-core contributor mode.
389
+
390
+ This prompt must never rely on private runtime state. It should inspect only
391
+ the isolated public repo checkout, make one coherent improvement, and end
392
+ by returning machine-readable summary JSON.
393
+ """
394
+
395
+ queued_section = ""
396
+ if queued_candidate:
397
+ queued_files = "\n".join(
398
+ f"- {path}" for path in (queued_candidate.get("files_changed") or [])[:20]
399
+ ) or "- (no files recorded)"
400
+ queued_source = str((queued_candidate.get("metadata") or {}).get("source") or "managed-runtime")
401
+ queued_section = f"""
402
+
403
+ PRIORITY PUBLIC-PORT QUEUE ITEM:
404
+ - Source: {queued_source}
405
+ - Title: {str(queued_candidate.get("title") or "").strip()}
406
+ - Why it matters: {str(queued_candidate.get("reasoning") or "").strip()}
407
+ - Files originally touched:
408
+ {queued_files}
409
+
410
+ This item was already fixed or detected outside the public contribution runner.
411
+ Before inventing another improvement, verify whether the public repository still
412
+ needs the same change and port it if necessary. If the repo is already correct,
413
+ make the smallest validating change that captures the same gap.
414
+ """
415
+
416
+ return f"""You are NEXO Public Evolution.
417
+
418
+ You are running inside an isolated checkout of the public NEXO repository.
419
+ Your job is to make one technically coherent improvement to the public core and
420
+ prepare it for a Draft PR.
421
+
422
+ STRICT RULES:
423
+ - Work only inside this repository checkout: {repo_root}
424
+ - You may modify only public core surfaces: src/, bin/, tests/, templates/, hooks/, migrations/, .claude-plugin/
425
+ - Do not read or use ~/.nexo, local DBs, personal scripts, emails, logs, prompts, secrets, or any user-identifying paths
426
+ - Do not push, open PRs, or change git remotes yourself
427
+ - Do not touch README, website, gh-pages, changelog, or release metadata in this mode
428
+ - Focus on one concrete improvement only
429
+ - Run validation for the files you touched
430
+
431
+ What to do:
432
+ 1. Inspect the repo and find a real, self-contained improvement in reliability, install/update behavior, cron recovery, diagnostics, hooks, tests, or other core infrastructure.
433
+ 2. Implement the change directly in this checkout.
434
+ 3. Run the smallest relevant validation commands.
435
+ 4. Return ONLY valid JSON with this shape:
436
+
437
+ {{
438
+ "title": "type: short title",
439
+ "problem": "what was wrong",
440
+ "summary": "what you changed",
441
+ "tests": ["command 1", "command 2"],
442
+ "risks": ["risk 1", "risk 2"]
443
+ }}
444
+
445
+ Cycle: #{cycle_number}
446
+ Quality over quantity. One strong improvement is better than three weak ones.
447
+ {queued_section}
448
+ """
449
+
450
+
451
+ def build_public_pr_review_prompt(
452
+ *,
453
+ pr_number: int,
454
+ title: str,
455
+ author: str,
456
+ url: str,
457
+ body: str,
458
+ files: list[str],
459
+ diff_text: str,
460
+ ) -> str:
461
+ """Prompt for peer-reviewing another public evolution PR.
462
+
463
+ This is used only when this machine already has its own Draft PR open, so
464
+ Evolution can still add value without opening a second PR.
465
+ """
466
+
467
+ rendered_files = "\n".join(f"- {path}" for path in files[:40]) if files else "- (no file list provided)"
468
+ trimmed_diff = (diff_text or "").strip()
469
+ if len(trimmed_diff) > 80000:
470
+ trimmed_diff = trimmed_diff[:80000] + "\n\n[diff truncated by NEXO]"
471
+
472
+ return f"""You are NEXO Public Evolution Review.
473
+
474
+ You are reviewing another opt-in public evolution PR. You must NOT merge, rebase,
475
+ push, or edit the PR. Your only job is to decide whether it deserves an approval
476
+ or whether it should receive a review comment without approval.
477
+
478
+ STRICT RULES:
479
+ - Review only this PR:
480
+ - Number: #{pr_number}
481
+ - Author: {author}
482
+ - URL: {url}
483
+ - Base the review only on the provided title, body, file list, and diff
484
+ - Do not assume hidden context
485
+ - If confidence is not strong, choose `comment`, not `approve`
486
+ - If the diff is too incomplete, too risky, or too ambiguous, choose `skip`
487
+ - Never suggest merge authority; maintainers decide that later
488
+ - Keep the review concise, technical, and useful
489
+
490
+ PR TITLE:
491
+ {title}
492
+
493
+ PR BODY:
494
+ {body or "(empty)"}
495
+
496
+ FILES CHANGED:
497
+ {rendered_files}
498
+
499
+ DIFF:
500
+ ```diff
501
+ {trimmed_diff or "(empty diff)"}
502
+ ```
503
+
504
+ Return ONLY valid JSON:
505
+ {{
506
+ "decision": "approve|comment|skip",
507
+ "summary": "one-line verdict",
508
+ "body": "the exact markdown text to post as the review body"
509
+ }}
510
+ """
511
+
512
+
513
+ def max_auto_changes(total_evolutions: int) -> int:
514
+ """Progressive trust: 1 for first 4 cycles, 2 for next 4, then 3."""
515
+ if total_evolutions < 4:
516
+ return 1
517
+ elif total_evolutions < 8:
518
+ return 2
519
+ return 3