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,266 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- NEXO Learning Validator — Cross-check findings against existing learnings.
4
-
5
- The wrapper collects the finding + current learnings from SQLite, then asks the
6
- configured automation backend whether the finding is already known, related, or
7
- genuinely new. If the backend is unavailable, it falls back to mechanical
8
- similarity matching.
9
-
10
- Usage as CLI:
11
- python3 nexo-learning-validator.py "finding text to validate"
12
- python3 nexo-learning-validator.py --category project "finding text"
13
-
14
- Usage as library:
15
- from nexo_learning_validator import validate_finding
16
- result = validate_finding("CRITICAL: message_id column is NULL")
17
- if result["known"]:
18
- print(f"Already known: {result['matching_learnings']}")
19
-
20
- Exit codes:
21
- 0 = Finding is NEW (not known)
22
- 1 = Finding is KNOWN (matches existing learning)
23
- """
24
-
25
- from __future__ import annotations
26
-
27
- import json
28
- import os
29
- import sqlite3
30
- import sys
31
- from pathlib import Path
32
-
33
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
34
- NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parents[1])))
35
- if str(NEXO_CODE) not in sys.path:
36
- sys.path.insert(0, str(NEXO_CODE))
37
-
38
- from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
39
-
40
- try:
41
- from client_preferences import resolve_user_model as _resolve_user_model
42
- _USER_MODEL = _resolve_user_model()
43
- except Exception:
44
- _USER_MODEL = ""
45
-
46
-
47
-
48
- NEXO_DB = NEXO_HOME / "data" / "nexo.db"
49
- JSON_ONLY_SYSTEM_PROMPT = (
50
- "Return exactly one valid JSON object. No markdown fences. No prose outside JSON."
51
- )
52
-
53
-
54
- def get_all_learnings(category: str | None = None) -> list[dict]:
55
- """Fetch all learnings from nexo.db."""
56
- conn = sqlite3.connect(str(NEXO_DB), timeout=10)
57
- conn.row_factory = sqlite3.Row
58
- if category:
59
- rows = conn.execute(
60
- "SELECT id, category, title, content FROM learnings WHERE category = ?",
61
- (category,),
62
- ).fetchall()
63
- else:
64
- rows = conn.execute(
65
- "SELECT id, category, title, content FROM learnings"
66
- ).fetchall()
67
- conn.close()
68
- return [dict(r) for r in rows]
69
-
70
-
71
- def _extract_json(text: str) -> dict | None:
72
- text = (text or "").strip()
73
- if not text:
74
- return None
75
- if text.startswith("```"):
76
- lines = text.splitlines()
77
- end = len(lines)
78
- for idx in range(len(lines) - 1, 0, -1):
79
- if lines[idx].strip() == "```":
80
- end = idx
81
- break
82
- text = "\n".join(lines[1:end]).strip()
83
- brace_start = text.find("{")
84
- if brace_start < 0:
85
- return None
86
- depth = 0
87
- for idx in range(brace_start, len(text)):
88
- if text[idx] == "{":
89
- depth += 1
90
- elif text[idx] == "}":
91
- depth -= 1
92
- if depth == 0:
93
- try:
94
- return json.loads(text[brace_start:idx + 1])
95
- except json.JSONDecodeError:
96
- return None
97
- return None
98
-
99
-
100
- def validate_finding(finding: str, category: str | None = None) -> dict:
101
- """
102
- Validate a finding against existing learnings.
103
-
104
- Returns:
105
- {
106
- "known": bool,
107
- "confidence": float (0-1),
108
- "matching_learnings": [{"id": int, "title": str, "similarity": float}],
109
- "recommendation": str
110
- }
111
- """
112
- learnings = get_all_learnings(category)
113
-
114
- if not learnings:
115
- return {
116
- "known": False,
117
- "confidence": 0,
118
- "matching_learnings": [],
119
- "recommendation": "No learnings in DB — finding is new by default",
120
- }
121
-
122
- learnings_ref = [
123
- {
124
- "id": l["id"],
125
- "cat": l["category"],
126
- "title": l["title"],
127
- "content": (l["content"] or "")[:300],
128
- }
129
- for l in learnings
130
- ]
131
-
132
- prompt = f"""You are a finding deduplication engine. Compare a new finding against existing learnings and determine if it's already known.
133
-
134
- NEW FINDING:
135
- {finding}
136
-
137
- EXISTING LEARNINGS ({len(learnings_ref)} total):
138
- {json.dumps(learnings_ref, indent=1, ensure_ascii=False)}
139
-
140
- Respond with ONLY valid JSON:
141
- {{
142
- "known": true/false,
143
- "confidence": 0.0-1.0,
144
- "matching_learnings": [
145
- {{"id": <learning_id>, "title": "<title>", "similarity": 0.0-1.0}}
146
- ],
147
- "recommendation": "<one line: KNOWN/LIKELY KNOWN/POSSIBLY RELATED/NEW>"
148
- }}
149
-
150
- Rules:
151
- - confidence >= 0.7 and same root cause = known: true
152
- - confidence 0.55-0.7 and related topic = known: true, say LIKELY KNOWN
153
- - confidence < 0.55 = known: false
154
- - Max 5 matching_learnings, sorted by similarity descending
155
- - If the finding describes the SAME bug/issue/pattern as a learning, it's known even if worded differently
156
- - Be strict: different symptoms of different bugs are NOT the same even if they mention the same file"""
157
-
158
- try:
159
- result = run_automation_prompt(
160
- prompt,
161
- model=_USER_MODEL or "sonnet",
162
- timeout=60,
163
- output_format="text",
164
- append_system_prompt=JSON_ONLY_SYSTEM_PROMPT,
165
- )
166
- parsed = _extract_json(result.stdout)
167
- if result.returncode == 0 and parsed:
168
- return parsed
169
- except AutomationBackendUnavailableError:
170
- pass
171
- except Exception:
172
- pass
173
-
174
- return _mechanical_validate(finding, learnings)
175
-
176
-
177
- def _mechanical_validate(finding: str, learnings: list[dict]) -> dict:
178
- """Fallback validation using SequenceMatcher when backend is unavailable."""
179
- from difflib import SequenceMatcher
180
-
181
- threshold = 0.45
182
- finding_kw = _extract_keywords(finding)
183
- matches = []
184
-
185
- for learning in learnings:
186
- title_sim = SequenceMatcher(None, finding.lower(), learning["title"].lower()).ratio()
187
- content_sim = SequenceMatcher(None, finding.lower(), (learning["content"] or "").lower()).ratio()
188
-
189
- learning_text = f"{learning['title']} {learning['content'] or ''}"
190
- learning_kw = _extract_keywords(learning_text)
191
- kw_overlap = len(finding_kw & learning_kw) / len(finding_kw) if finding_kw and learning_kw else 0
192
-
193
- combined = max(title_sim, content_sim) * 0.6 + kw_overlap * 0.4
194
-
195
- if combined >= threshold:
196
- matches.append({
197
- "id": learning["id"],
198
- "category": learning["category"],
199
- "title": learning["title"],
200
- "similarity": round(combined, 3),
201
- })
202
-
203
- matches.sort(key=lambda x: x["similarity"], reverse=True)
204
- top = matches[:5]
205
-
206
- if not top:
207
- return {"known": False, "confidence": 0, "matching_learnings": [], "recommendation": "NEW finding"}
208
-
209
- best = top[0]["similarity"]
210
- if best >= 0.7:
211
- return {"known": True, "confidence": best, "matching_learnings": top,
212
- "recommendation": f"KNOWN issue (learning #{top[0]['id']})"}
213
- if best >= 0.55:
214
- return {"known": True, "confidence": best, "matching_learnings": top,
215
- "recommendation": f"LIKELY KNOWN (learning #{top[0]['id']})"}
216
- return {"known": False, "confidence": best, "matching_learnings": top,
217
- "recommendation": "POSSIBLY RELATED but different enough to report"}
218
-
219
-
220
- def _extract_keywords(text: str) -> set:
221
- """Extract meaningful keywords from text."""
222
- stop_words = {
223
- "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
224
- "have", "has", "had", "do", "does", "did", "will", "would", "could",
225
- "should", "may", "might", "must", "shall", "can", "need", "dare",
226
- "to", "of", "in", "for", "on", "with", "at", "by", "from", "as",
227
- "and", "but", "or", "nor", "not", "so", "yet", "both", "either",
228
- "error", "critical", "warning", "bug", "issue", "problem", "fix",
229
- "el", "la", "los", "las", "un", "una", "de", "en", "que", "por",
230
- }
231
- words = set()
232
- for word in text.lower().split():
233
- clean = "".join(c for c in word if c.isalnum() or c == "_")
234
- if clean and len(clean) > 2 and clean not in stop_words:
235
- words.add(clean)
236
- return words
237
-
238
-
239
- def main():
240
- import argparse
241
-
242
- parser = argparse.ArgumentParser(description="Validate findings against existing NEXO learnings")
243
- parser.add_argument("finding", help="The finding text to validate")
244
- parser.add_argument("--category", "-c", help="Filter learnings by category")
245
- parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
246
- args = parser.parse_args()
247
-
248
- result = validate_finding(args.finding, args.category)
249
-
250
- if args.json:
251
- print(json.dumps(result, indent=2, ensure_ascii=False))
252
- else:
253
- status = "KNOWN" if result["known"] else "NEW"
254
- print(f"Status: {status} (confidence: {result['confidence']:.0%})")
255
- print(f"Recommendation: {result['recommendation']}")
256
- if result["matching_learnings"]:
257
- print("Related learnings:")
258
- for match in result["matching_learnings"]:
259
- cat = match.get("category", "?")
260
- print(f" #{match['id']} [{cat}] {match['title']} ({match['similarity']:.0%})")
261
-
262
- sys.exit(1 if result["known"] else 0)
263
-
264
-
265
- if __name__ == "__main__":
266
- main()
@@ -1,260 +0,0 @@
1
- #!/usr/bin/env python3
2
- """NEXO Migration Tool — automatic, idempotent upgrades between versions.
3
-
4
- Usage:
5
- python3 nexo-migrate.py # auto-detect current → target
6
- python3 nexo-migrate.py --dry-run # show what would happen
7
- python3 nexo-migrate.py --from 1.6.0 # override detected current version
8
-
9
- Reads current version from $NEXO_HOME/version.json.
10
- Reads target version from the installed runtime package.json.
11
- Backs up NEXO_HOME/db/ before any migration.
12
- Runs DB schema migrations via the existing _schema.py system.
13
- """
14
-
15
- import argparse
16
- import json
17
- import os
18
- import shutil
19
- import sys
20
- from datetime import datetime
21
- from pathlib import Path
22
-
23
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))).expanduser()
24
- SCRIPT_ROOT = Path(__file__).resolve().parent.parent
25
-
26
-
27
- def _resolve_runtime_root() -> Path:
28
- candidates: list[Path] = []
29
-
30
- raw_code = os.environ.get("NEXO_CODE", "").strip()
31
- if raw_code:
32
- env_code = Path(raw_code).expanduser()
33
- candidates.append(env_code)
34
- if env_code.name == "src":
35
- candidates.append(env_code.parent)
36
-
37
- candidates.extend([SCRIPT_ROOT, NEXO_HOME])
38
-
39
- seen: set[str] = set()
40
- for candidate in candidates:
41
- candidate = candidate if candidate.is_dir() else candidate.parent
42
- key = str(candidate)
43
- if key in seen:
44
- continue
45
- seen.add(key)
46
- if (candidate / "package.json").is_file():
47
- return candidate
48
- if (candidate / "src" / "server.py").is_file() and (candidate / "package.json").is_file():
49
- return candidate
50
-
51
- return SCRIPT_ROOT
52
-
53
-
54
- RUNTIME_ROOT = _resolve_runtime_root()
55
- SOURCE_ROOT = RUNTIME_ROOT / "src" if (RUNTIME_ROOT / "src" / "db").is_dir() else RUNTIME_ROOT
56
-
57
-
58
- # ── Version helpers ──────────────────────────────────────────────
59
-
60
- def parse_version(v: str) -> tuple:
61
- """Parse '1.7.0-beta.1' → (1, 7, 0, 'beta.1'). Pre-release is optional."""
62
- parts = v.strip().lstrip("v").split("-", 1)
63
- nums = tuple(int(x) for x in parts[0].split("."))
64
- pre = parts[1] if len(parts) > 1 else ""
65
- return (*nums, pre)
66
-
67
-
68
- def version_key(v: str) -> tuple:
69
- """Sortable key: releases sort after pre-releases of same version."""
70
- nums = parse_version(v)
71
- # Empty pre-release string sorts AFTER any pre-release tag
72
- pre = nums[3] if len(nums) > 3 else ""
73
- return (nums[0], nums[1], nums[2], 0 if pre else 1, pre)
74
-
75
-
76
- def get_current_version() -> str:
77
- """Read installed version from NEXO_HOME/version.json."""
78
- vfile = NEXO_HOME / "version.json"
79
- if not vfile.exists():
80
- return "0.0.0"
81
- try:
82
- data = json.loads(vfile.read_text())
83
- return data.get("version", "0.0.0")
84
- except Exception:
85
- return "0.0.0"
86
-
87
-
88
- def get_target_version() -> str:
89
- """Read target version from installed runtime package.json."""
90
- pkg = RUNTIME_ROOT / "package.json"
91
- if not pkg.exists():
92
- print(f"ERROR: package.json not found at {pkg}", file=sys.stderr)
93
- sys.exit(1)
94
- data = json.loads(pkg.read_text())
95
- return data["version"]
96
-
97
-
98
- # ── Backup ───────────────────────────────────────────────────────
99
-
100
- def backup_databases() -> str:
101
- """Backup all .db files before migration. Returns backup dir path."""
102
- ts = datetime.now().strftime("%Y%m%d-%H%M%S")
103
- backup_dir = NEXO_HOME / "backups" / f"pre-migrate-{ts}"
104
- backup_dir.mkdir(parents=True, exist_ok=True)
105
-
106
- data_dir = NEXO_HOME / "data"
107
- if data_dir.exists():
108
- for db_file in data_dir.glob("*.db*"):
109
- shutil.copy2(db_file, backup_dir / db_file.name)
110
- # Also check legacy db/ location
111
- legacy_db_dir = NEXO_HOME / "db"
112
- if legacy_db_dir.exists():
113
- for db_file in legacy_db_dir.glob("*.db*"):
114
- if not (backup_dir / db_file.name).exists():
115
- shutil.copy2(db_file, backup_dir / db_file.name)
116
-
117
- # Also backup version.json
118
- vfile = NEXO_HOME / "version.json"
119
- if vfile.exists():
120
- shutil.copy2(vfile, backup_dir / "version.json")
121
-
122
- return str(backup_dir)
123
-
124
-
125
- # ── Migration steps ──────────────────────────────────────────────
126
-
127
- def ensure_nexo_home_dirs():
128
- """Create all required NEXO_HOME subdirectories."""
129
- dirs = [
130
- "db", "brain", "logs", "operations", "coordination",
131
- "scripts", "hooks", "plugins", "backups", "memory",
132
- "docs", "projects", "learnings", "agents", "skills",
133
- ]
134
- for d in dirs:
135
- (NEXO_HOME / d).mkdir(parents=True, exist_ok=True)
136
-
137
-
138
- def run_db_schema_migrations():
139
- """Run the formal DB schema migration system from _schema.py."""
140
- if str(SOURCE_ROOT) not in sys.path:
141
- sys.path.insert(0, str(SOURCE_ROOT))
142
-
143
- # Set NEXO_HOME env for the db module
144
- os.environ["NEXO_HOME"] = str(NEXO_HOME)
145
- os.environ["NEXO_SKIP_FS_INDEX"] = "1" # Don't rebuild FTS during migration
146
-
147
- try:
148
- from db import init_db
149
- init_db()
150
- print(" DB schema migrations applied.")
151
- except Exception as e:
152
- print(f" WARNING: DB schema migration error: {e}", file=sys.stderr)
153
-
154
-
155
- def write_version_json(version: str):
156
- """Write version.json with the installed version."""
157
- vfile = NEXO_HOME / "version.json"
158
- data = {
159
- "version": version,
160
- "installed_at": datetime.now().isoformat(timespec="seconds"),
161
- "nexo_home": str(NEXO_HOME),
162
- }
163
- vfile.write_text(json.dumps(data, indent=2) + "\n")
164
-
165
-
166
- # ── Migration registry ───────────────────────────────────────────
167
- # Each entry: version → list of (description, callable)
168
- # Migrations run for all versions > current AND <= target.
169
-
170
- def _migrate_1_7_0():
171
- """1.7.0: Ensure NEXO_HOME paths, create directories, update version."""
172
- ensure_nexo_home_dirs()
173
- run_db_schema_migrations()
174
- print(" Created/verified all NEXO_HOME directories.")
175
-
176
-
177
- MIGRATION_REGISTRY: dict[str, list[tuple[str, callable]]] = {
178
- "1.7.0": [
179
- ("Ensure NEXO_HOME dirs + DB schema", _migrate_1_7_0),
180
- ],
181
- }
182
-
183
-
184
- # ── Main ─────────────────────────────────────────────────────────
185
-
186
- def get_applicable_migrations(current: str, target: str) -> list[tuple[str, str, callable]]:
187
- """Return list of (version, description, fn) for migrations between current and target."""
188
- current_key = version_key(current)
189
- target_key = version_key(target)
190
-
191
- applicable = []
192
- for ver, steps in sorted(MIGRATION_REGISTRY.items(), key=lambda x: version_key(x[0])):
193
- ver_key = version_key(ver)
194
- # Run if version > current and <= target (base version comparison)
195
- base_ver = ver.split("-")[0] # strip pre-release for comparison
196
- base_ver_key = version_key(base_ver)
197
- if base_ver_key > (current_key[0], current_key[1], current_key[2], current_key[3], current_key[4] if len(current_key) > 4 else ""):
198
- if base_ver_key <= (target_key[0], target_key[1], target_key[2], 1, ""):
199
- for desc, fn in steps:
200
- applicable.append((ver, desc, fn))
201
-
202
- return applicable
203
-
204
-
205
- def main():
206
- parser = argparse.ArgumentParser(description="NEXO Migration Tool")
207
- parser.add_argument("--dry-run", action="store_true", help="Show what would happen without executing")
208
- parser.add_argument("--from", dest="from_ver", help="Override detected current version")
209
- args = parser.parse_args()
210
-
211
- current = args.from_ver or get_current_version()
212
- target = get_target_version()
213
-
214
- print(f"NEXO Migration: {current} → {target}")
215
- print(f"NEXO_HOME: {NEXO_HOME}")
216
- print()
217
-
218
- if version_key(current) >= version_key(target):
219
- print("Already up to date. Nothing to migrate.")
220
- return
221
-
222
- migrations = get_applicable_migrations(current, target)
223
- if not migrations:
224
- print("No migration steps needed (only version bump).")
225
- else:
226
- print(f"Migrations to run ({len(migrations)}):")
227
- for ver, desc, _ in migrations:
228
- print(f" [{ver}] {desc}")
229
- print()
230
-
231
- if args.dry_run:
232
- print("DRY RUN — no changes made.")
233
- return
234
-
235
- # Backup before anything
236
- backup_path = backup_databases()
237
- print(f"Backup created: {backup_path}")
238
- print()
239
-
240
- # Ensure base directories exist
241
- ensure_nexo_home_dirs()
242
-
243
- # Run migrations
244
- for ver, desc, fn in migrations:
245
- print(f"Running [{ver}] {desc}...")
246
- try:
247
- fn()
248
- print(f" Done.")
249
- except Exception as e:
250
- print(f" ERROR: {e}", file=sys.stderr)
251
- print(f" Backup at: {backup_path}", file=sys.stderr)
252
- sys.exit(1)
253
-
254
- # Write final version
255
- write_version_json(target)
256
- print(f"\nMigration complete: {current} → {target}")
257
-
258
-
259
- if __name__ == "__main__":
260
- main()
@@ -1,127 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
- """NEXO Outcome Checker — daily verification of pending tracked outcomes."""
4
-
5
- import json
6
- import os
7
- import sys
8
- from datetime import datetime
9
- from pathlib import Path
10
-
11
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
12
- _script_dir = Path(__file__).resolve().parent
13
- _repo_src = _script_dir.parent
14
- NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
15
- if str(NEXO_CODE) not in sys.path:
16
- sys.path.insert(0, str(NEXO_CODE))
17
-
18
- import db as nexo_db
19
-
20
- LOG_FILE = NEXO_HOME / "logs" / "outcome-checker.log"
21
- SUMMARY_FILE = NEXO_HOME / "coordination" / "outcome-checker-summary.json"
22
-
23
-
24
- def log(message: str):
25
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
26
- line = f"[{ts}] {message}"
27
- print(line, flush=True)
28
- LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
29
- with open(LOG_FILE, "a") as handle:
30
- handle.write(line + "\n")
31
-
32
-
33
- def main() -> int:
34
- log("=== Outcome Checker starting ===")
35
- nexo_db.init_db()
36
-
37
- due_rows = nexo_db.pending_outcomes_due(limit=500)
38
- if not due_rows:
39
- summary = {
40
- "checked_at": datetime.now().isoformat(timespec="seconds"),
41
- "checked": 0,
42
- "met": 0,
43
- "missed": 0,
44
- "pending": 0,
45
- "errors": 0,
46
- "ids": [],
47
- }
48
- SUMMARY_FILE.parent.mkdir(parents=True, exist_ok=True)
49
- SUMMARY_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
50
- log("No pending due outcomes.")
51
- log("=== Outcome Checker complete ===")
52
- return 0
53
-
54
- checked = met = missed = pending = errors = 0
55
- checked_ids: list[int] = []
56
- for row in due_rows:
57
- checked += 1
58
- checked_ids.append(int(row["id"]))
59
- result = nexo_db.evaluate_outcome(int(row["id"]), create_learning_on_miss=True)
60
- if "error" in result:
61
- errors += 1
62
- log(f"ERROR outcome #{row['id']}: {result['error']}")
63
- continue
64
- status = result.get("status", "pending")
65
- if status == "met":
66
- met += 1
67
- elif status == "missed":
68
- missed += 1
69
- else:
70
- pending += 1
71
- log(
72
- f"Outcome #{row['id']} -> {status} "
73
- f"(action={result.get('action_type')}:{result.get('action_id') or '—'}, "
74
- f"deadline={result.get('deadline')})"
75
- )
76
-
77
- # Phase 2 item 2: after closing outcomes, attempt to promote any
78
- # outcome pattern that just crossed the suggested-skill threshold to a
79
- # real draft skill. The helper is idempotent and capped at
80
- # max_promotions per run, so this is safe to call on every cycle.
81
- promotion_summary: dict = {"promoted": [], "skipped": [], "errors": [], "scanned": 0}
82
- try:
83
- from skills_runtime import auto_promote_outcome_patterns_to_skills
84
- promotion_summary = auto_promote_outcome_patterns_to_skills(
85
- min_success_rate=0.8,
86
- max_promotions=3,
87
- )
88
- if promotion_summary.get("promoted"):
89
- log(
90
- f"Auto-promoted {len(promotion_summary['promoted'])} outcome pattern(s) "
91
- f"to skill draft(s) (scanned={promotion_summary.get('scanned', 0)})"
92
- )
93
- for entry in promotion_summary["promoted"]:
94
- log(
95
- f" -> {entry.get('pattern_key')} -> skill {entry.get('skill_id')} "
96
- f"(success_rate={entry.get('success_rate')}, created={entry.get('created')})"
97
- )
98
- elif promotion_summary.get("scanned"):
99
- log(
100
- f"Outcome pattern auto-promote: scanned {promotion_summary['scanned']}, "
101
- f"none qualified (skipped={len(promotion_summary['skipped'])})"
102
- )
103
- except Exception as e:
104
- log(f"WARN: outcome pattern auto-promote raised: {e}")
105
-
106
- summary = {
107
- "checked_at": datetime.now().isoformat(timespec="seconds"),
108
- "checked": checked,
109
- "met": met,
110
- "missed": missed,
111
- "pending": pending,
112
- "errors": errors,
113
- "ids": checked_ids,
114
- "auto_promoted_patterns": promotion_summary,
115
- }
116
- SUMMARY_FILE.parent.mkdir(parents=True, exist_ok=True)
117
- SUMMARY_FILE.write_text(json.dumps(summary, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
118
- log(
119
- f"Summary: checked={checked} met={met} missed={missed} "
120
- f"pending={pending} errors={errors}"
121
- )
122
- log("=== Outcome Checker complete ===")
123
- return 1 if errors else 0
124
-
125
-
126
- if __name__ == "__main__":
127
- raise SystemExit(main())