nexo-brain 5.3.26 → 5.3.27

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