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,519 +0,0 @@
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