nexo-brain 2.1.0 → 2.3.0

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 (297) hide show
  1. package/README.md +7 -7
  2. package/bin/nexo-brain.js +53 -26
  3. package/package.json +1 -1
  4. package/scripts/migrate-to-unified 2.sh +813 -0
  5. package/scripts/migrate-v1.5-to-v1.6 2.py +778 -0
  6. package/scripts/migrate-v1.7-to-v1.8 2.py +214 -0
  7. package/scripts/migrate-v1.7-to-v1.8.py +2 -2
  8. package/scripts/nexo-preflight.sh +236 -0
  9. package/scripts/pre-commit-check 2.sh +55 -0
  10. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  11. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  12. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  13. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  14. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  15. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  16. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  17. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  18. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  19. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  20. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  21. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  22. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  23. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  24. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  25. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  26. package/src/auto_close_sessions 2.py +159 -0
  27. package/src/auto_update 2.py +634 -0
  28. package/src/auto_update.py +25 -0
  29. package/src/claim_graph 2.py +323 -0
  30. package/src/cognitive/__init__ 2.py +62 -0
  31. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  32. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  33. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  34. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  35. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  36. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  37. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  38. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  39. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  40. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  41. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  42. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  43. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  44. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  45. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  46. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  47. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  48. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  49. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  50. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  51. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  52. package/src/cognitive/_core 2.py +567 -0
  53. package/src/cognitive/_decay 2.py +382 -0
  54. package/src/cognitive/_ingest 2.py +892 -0
  55. package/src/cognitive/_memory 2.py +912 -0
  56. package/src/cognitive/_search 2.py +949 -0
  57. package/src/cognitive/_trust 2.py +464 -0
  58. package/src/cognitive/_trust.py +10 -36
  59. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  60. package/src/crons/manifest 2.json +106 -0
  61. package/src/crons/manifest.json +6 -13
  62. package/src/crons/sync 2.py +217 -0
  63. package/src/crons/sync.py +151 -6
  64. package/src/dashboard/__init__ 2.py +0 -0
  65. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  66. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  67. package/src/dashboard/app 2.py +789 -0
  68. package/src/db/__init__ 2.py +89 -0
  69. package/src/db/__init__.py +13 -0
  70. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  71. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  72. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  73. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  74. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  75. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  76. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  77. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  78. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  79. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  80. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  81. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  82. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  83. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  84. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  85. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  86. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  87. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  111. package/src/db/_core 2.py +417 -0
  112. package/src/db/_credentials 2.py +124 -0
  113. package/src/db/_cron_runs.py +74 -0
  114. package/src/db/_entities 2.py +178 -0
  115. package/src/db/_episodic 2.py +738 -0
  116. package/src/db/_episodic.py +40 -6
  117. package/src/db/_evolution 2.py +54 -0
  118. package/src/db/_fts 2.py +406 -0
  119. package/src/db/_learnings 2.py +168 -0
  120. package/src/db/_reminders 2.py +338 -0
  121. package/src/db/_schema 2.py +364 -0
  122. package/src/db/_schema.py +64 -0
  123. package/src/db/_sessions 2.py +300 -0
  124. package/src/db/_skills.py +514 -0
  125. package/src/db/_tasks 2.py +91 -0
  126. package/src/evolution_cycle 2.py +266 -0
  127. package/src/hnsw_index 2.py +254 -0
  128. package/src/hooks/auto_capture 2.py +208 -0
  129. package/src/hooks/caffeinate-guard 2.sh +8 -0
  130. package/src/hooks/capture-session 2.sh +21 -0
  131. package/src/hooks/capture-session.sh +2 -0
  132. package/src/hooks/capture-tool-logs 2.sh +127 -0
  133. package/src/hooks/capture-tool-logs.sh +3 -2
  134. package/src/hooks/daily-briefing-check 2.sh +33 -0
  135. package/src/hooks/inbox-hook 2.sh +76 -0
  136. package/src/hooks/inbox-hook.sh +3 -2
  137. package/src/hooks/post-compact 2.sh +148 -0
  138. package/src/hooks/post-compact.sh +1 -1
  139. package/src/hooks/pre-compact 2.sh +151 -0
  140. package/src/hooks/pre-compact.sh +1 -1
  141. package/src/hooks/session-start 2.sh +268 -0
  142. package/src/hooks/session-start.sh +6 -3
  143. package/src/hooks/session-stop 2.sh +140 -0
  144. package/src/hooks/session-stop.sh +14 -102
  145. package/src/kg_populate 2.py +290 -0
  146. package/src/knowledge_graph 2.py +257 -0
  147. package/src/maintenance 2.py +59 -0
  148. package/src/migrate_embeddings 2.py +122 -0
  149. package/src/plugin_loader 2.py +202 -0
  150. package/src/plugins/__init__ 2.py +0 -0
  151. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  152. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  155. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  156. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  157. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  183. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  185. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  187. package/src/plugins/adaptive_mode 2.py +805 -0
  188. package/src/plugins/agents 2.py +52 -0
  189. package/src/plugins/artifact_registry 2.py +450 -0
  190. package/src/plugins/backup 2.py +104 -0
  191. package/src/plugins/cognitive_memory 2.py +564 -0
  192. package/src/plugins/core_rules 2.py +252 -0
  193. package/src/plugins/cortex 2.py +299 -0
  194. package/src/plugins/entities 2.py +67 -0
  195. package/src/plugins/episodic_memory 2.py +533 -0
  196. package/src/plugins/episodic_memory.py +5 -3
  197. package/src/plugins/evolution 2.py +115 -0
  198. package/src/plugins/guard 2.py +746 -0
  199. package/src/plugins/knowledge_graph_tools 2.py +105 -0
  200. package/src/plugins/preferences 2.py +47 -0
  201. package/src/plugins/schedule.py +212 -0
  202. package/src/plugins/skills.py +264 -0
  203. package/src/plugins/update 2.py +256 -0
  204. package/src/requirements 2.txt +12 -0
  205. package/src/rules/__init__ 2.py +0 -0
  206. package/src/rules/core-rules 2.json +331 -0
  207. package/src/rules/migrate 2.py +207 -0
  208. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  209. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  210. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  211. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  216. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  217. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  218. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  219. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  220. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  221. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  222. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  223. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  229. package/src/scripts/check-context 2.py +264 -0
  230. package/src/scripts/deep-sleep/apply_findings.py +168 -8
  231. package/src/scripts/deep-sleep/collect.py +33 -11
  232. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  233. package/src/scripts/deep-sleep/extract.py +80 -8
  234. package/src/scripts/deep-sleep/synthesize-prompt.md +59 -2
  235. package/src/scripts/deep-sleep/synthesize.py +3 -1
  236. package/src/scripts/nexo-auto-update 2.py +6 -0
  237. package/src/scripts/nexo-backup 2.sh +25 -0
  238. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  239. package/src/scripts/nexo-catchup 2.py +242 -0
  240. package/src/scripts/nexo-catchup.py +65 -29
  241. package/src/scripts/nexo-cognitive-decay 2.py +182 -0
  242. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  243. package/src/scripts/nexo-daily-self-audit 2.py +552 -0
  244. package/src/scripts/nexo-daily-self-audit.py +4 -2
  245. package/src/scripts/nexo-deep-sleep 2.sh +97 -0
  246. package/src/scripts/nexo-deep-sleep.sh +66 -77
  247. package/src/scripts/nexo-evolution-run 2.py +597 -0
  248. package/src/scripts/nexo-evolution-run.py +13 -0
  249. package/src/scripts/nexo-followup-hygiene 2.py +112 -0
  250. package/src/scripts/nexo-immune 2.py +927 -0
  251. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  252. package/src/scripts/nexo-install 2.py +6 -0
  253. package/src/scripts/nexo-learning-housekeep 2.py +245 -0
  254. package/src/scripts/nexo-learning-housekeep.py +156 -1
  255. package/src/scripts/nexo-learning-validator 2.py +207 -0
  256. package/src/scripts/nexo-learning-validator.py +19 -0
  257. package/src/scripts/nexo-migrate 2.py +232 -0
  258. package/src/scripts/nexo-postmortem-consolidator 2.py +421 -0
  259. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  260. package/src/scripts/nexo-pre-commit 2.py +120 -0
  261. package/src/scripts/nexo-prevent-sleep 2.sh +29 -0
  262. package/src/scripts/nexo-proactive-dashboard 2.py +345 -0
  263. package/src/scripts/nexo-reflection 2.py +253 -0
  264. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  265. package/src/scripts/nexo-send-email 2.py +25 -0
  266. package/src/scripts/nexo-send-reply 2.py +178 -0
  267. package/src/scripts/nexo-sleep 2.py +592 -0
  268. package/src/scripts/nexo-sleep.py +16 -11
  269. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  270. package/src/scripts/nexo-synthesis 2.py +253 -0
  271. package/src/scripts/nexo-synthesis.py +46 -3
  272. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  273. package/src/scripts/nexo-update 2.sh +161 -0
  274. package/src/scripts/nexo-watchdog 2.sh +878 -0
  275. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  276. package/src/scripts/nexo-watchdog.sh +72 -19
  277. package/src/server 2.py +733 -0
  278. package/src/server.py +11 -2
  279. package/src/storage_router 2.py +32 -0
  280. package/src/tools_coordination 2.py +102 -0
  281. package/src/tools_credentials 2.py +68 -0
  282. package/src/tools_learnings 2.py +220 -0
  283. package/src/tools_menu 2.py +227 -0
  284. package/src/tools_reminders 2.py +86 -0
  285. package/src/tools_reminders_crud 2.py +159 -0
  286. package/src/tools_reminders_crud.py +7 -0
  287. package/src/tools_sessions 2.py +476 -0
  288. package/src/tools_task_history 2.py +57 -0
  289. package/templates/CLAUDE.md 2.template +63 -0
  290. package/templates/openclaw 2.json +13 -0
  291. package/tests/__init__ 2.py +0 -0
  292. package/tests/conftest 2.py +71 -0
  293. package/tests/test_cognitive 2.py +205 -0
  294. package/tests/test_knowledge_graph 2.py +140 -0
  295. package/tests/test_migrations 2.py +137 -0
  296. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  297. /package/src/scripts/{nexo-github-monitor.py → nexo-github-monitor 2.py} +0 -0
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """NEXO Brain Rules Migration System.
3
+
4
+ Manages versioned core rules that ship with every installation.
5
+ Handles adding new rules, removing deprecated ones, and updating
6
+ the user's CLAUDE.md without touching their customizations.
7
+
8
+ Usage:
9
+ from rules.migrate import migrate_rules
10
+ result = migrate_rules(nexo_home) # Returns dict with changes applied
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import re
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+
20
+ RULES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "core-rules.json")
21
+ VERSION_KEY = "rules_version"
22
+
23
+
24
+ def load_core_rules() -> dict:
25
+ """Load the current core rules definition."""
26
+ with open(RULES_FILE, "r") as f:
27
+ return json.load(f)
28
+
29
+
30
+ def get_installed_version(nexo_home: str) -> Optional[str]:
31
+ """Get the rules version currently installed in the user's NEXO home."""
32
+ version_file = os.path.join(nexo_home, "brain", "rules_version.json")
33
+ if not os.path.exists(version_file):
34
+ return None
35
+ try:
36
+ with open(version_file, "r") as f:
37
+ data = json.load(f)
38
+ return data.get("version")
39
+ except (json.JSONDecodeError, KeyError):
40
+ return None
41
+
42
+
43
+ def save_installed_version(nexo_home: str, version: str, rule_ids: list[str]):
44
+ """Record which rules version and rule IDs are installed."""
45
+ version_file = os.path.join(nexo_home, "brain", "rules_version.json")
46
+ os.makedirs(os.path.dirname(version_file), exist_ok=True)
47
+ data = {
48
+ "version": version,
49
+ "installed_rule_ids": rule_ids,
50
+ "installed_at": _now_iso(),
51
+ }
52
+ with open(version_file, "w") as f:
53
+ json.dump(data, f, indent=2)
54
+
55
+
56
+ def get_installed_rule_ids(nexo_home: str) -> list[str]:
57
+ """Get the list of rule IDs currently installed."""
58
+ version_file = os.path.join(nexo_home, "brain", "rules_version.json")
59
+ if not os.path.exists(version_file):
60
+ return []
61
+ try:
62
+ with open(version_file, "r") as f:
63
+ data = json.load(f)
64
+ return data.get("installed_rule_ids", [])
65
+ except (json.JSONDecodeError, KeyError):
66
+ return []
67
+
68
+
69
+ def generate_rules_markdown(rules_data: dict) -> str:
70
+ """Generate the Operational Codex markdown from core-rules.json."""
71
+ lines = [
72
+ "## Operational Codex (NON-NEGOTIABLE)",
73
+ "",
74
+ "These rules are the behavioral foundation of every cognitive co-operator.",
75
+ "They are derived from real production failures and validated through multi-AI debate.",
76
+ f"Rules version: {rules_data['_meta']['version']}",
77
+ "",
78
+ ]
79
+
80
+ for cat_key, cat in rules_data["categories"].items():
81
+ lines.append(f"### {cat['label']}")
82
+ lines.append("")
83
+ for rule in cat["rules"]:
84
+ tag = "BLOCKING" if rule["type"] == "blocking" else "ADVISORY"
85
+ lines.append(f"**{rule['id']}. {rule['rule']}** [{tag}]")
86
+ lines.append(f"_{rule['why']}_")
87
+ lines.append("")
88
+
89
+ return "\n".join(lines)
90
+
91
+
92
+ def find_codex_section(claude_md: str) -> tuple[int, int]:
93
+ """Find the start and end positions of the Operational Codex section in CLAUDE.md."""
94
+ # Look for the section header
95
+ start_pattern = r"## Operational Codex \(NON-NEGOTIABLE\)"
96
+ start_match = re.search(start_pattern, claude_md)
97
+ if not start_match:
98
+ return (-1, -1)
99
+
100
+ start = start_match.start()
101
+
102
+ # Find the next ## section header after the codex
103
+ rest = claude_md[start_match.end():]
104
+ next_section = re.search(r"\n## [A-Z]", rest)
105
+ if next_section:
106
+ end = start_match.end() + next_section.start()
107
+ else:
108
+ end = len(claude_md)
109
+
110
+ return (start, end)
111
+
112
+
113
+ def migrate_rules(nexo_home: str, dry_run: bool = False) -> dict:
114
+ """Migrate rules to the latest version.
115
+
116
+ Compares installed rules version with current core-rules.json.
117
+ Adds new rules, removes deprecated ones, updates CLAUDE.md.
118
+
119
+ Args:
120
+ nexo_home: Path to NEXO home directory
121
+ dry_run: If True, show what would change without applying
122
+
123
+ Returns:
124
+ Dict with: version_from, version_to, added, removed, unchanged, dry_run
125
+ """
126
+ rules_data = load_core_rules()
127
+ current_version = rules_data["_meta"]["version"]
128
+ installed_version = get_installed_version(nexo_home)
129
+ installed_ids = set(get_installed_rule_ids(nexo_home))
130
+
131
+ # Collect all rule IDs from current version
132
+ current_ids = set()
133
+ for cat in rules_data["categories"].values():
134
+ for rule in cat["rules"]:
135
+ current_ids.add(rule["id"])
136
+
137
+ # Calculate diff
138
+ added = current_ids - installed_ids if installed_ids else current_ids
139
+ removed = installed_ids - current_ids if installed_ids else set()
140
+ unchanged = current_ids & installed_ids if installed_ids else set()
141
+
142
+ result = {
143
+ "version_from": installed_version or "none",
144
+ "version_to": current_version,
145
+ "added": sorted(added),
146
+ "removed": sorted(removed),
147
+ "unchanged": sorted(unchanged),
148
+ "total_rules": len(current_ids),
149
+ "dry_run": dry_run,
150
+ }
151
+
152
+ if installed_version == current_version and not added and not removed:
153
+ result["status"] = "up_to_date"
154
+ return result
155
+
156
+ if dry_run:
157
+ result["status"] = "changes_pending"
158
+ return result
159
+
160
+ # Apply: update the Operational Codex section in CLAUDE.md
161
+ claude_md_path = os.path.join(nexo_home, "CLAUDE.md")
162
+ if os.path.exists(claude_md_path):
163
+ with open(claude_md_path, "r") as f:
164
+ claude_md = f.read()
165
+
166
+ new_codex = generate_rules_markdown(rules_data)
167
+ start, end = find_codex_section(claude_md)
168
+
169
+ if start >= 0:
170
+ # Replace existing codex section
171
+ claude_md = claude_md[:start] + new_codex + "\n" + claude_md[end:]
172
+ else:
173
+ # Append codex after the first section
174
+ # Find the end of the first ## section
175
+ first_section_end = re.search(r"\n## ", claude_md[10:])
176
+ if first_section_end:
177
+ insert_pos = 10 + first_section_end.start()
178
+ claude_md = claude_md[:insert_pos] + "\n\n" + new_codex + "\n" + claude_md[insert_pos:]
179
+ else:
180
+ claude_md += "\n\n" + new_codex
181
+
182
+ with open(claude_md_path, "w") as f:
183
+ f.write(claude_md)
184
+
185
+ # Save version record
186
+ save_installed_version(nexo_home, current_version, sorted(current_ids))
187
+
188
+ result["status"] = "migrated"
189
+ return result
190
+
191
+
192
+ def _now_iso() -> str:
193
+ from datetime import datetime
194
+ return datetime.utcnow().isoformat() + "Z"
195
+
196
+
197
+ if __name__ == "__main__":
198
+ import sys
199
+ if len(sys.argv) < 2:
200
+ print("Usage: python migrate.py <nexo_home> [--dry-run]")
201
+ sys.exit(1)
202
+
203
+ home = sys.argv[1]
204
+ dry = "--dry-run" in sys.argv
205
+
206
+ result = migrate_rules(home, dry_run=dry)
207
+ print(json.dumps(result, indent=2))
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env python3
2
+ """Context checker for NEXO operations - prevents duplicate actions.
3
+
4
+ Mechanical checks (email sent, file exists, action done) run in Python.
5
+ When the 'smart' command is used, passes context to Claude CLI for
6
+ intelligent duplicate/conflict detection that goes beyond file checks.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import json
12
+ import hashlib
13
+ import subprocess
14
+ from datetime import datetime
15
+ from pathlib import Path
16
+
17
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
18
+
19
+ CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
20
+
21
+ class ContextChecker:
22
+ def __init__(self):
23
+ self.state_dir = NEXO_HOME / 'state'
24
+ self.state_dir.mkdir(exist_ok=True)
25
+
26
+ def check_email_sent(self, to_addr, subject, since_hours=72):
27
+ """Check if email was already sent to address with subject."""
28
+ sent_path = Path.home() / 'mail' / '.nexo-sent' / '.Sent' # Configure for your mail setup
29
+ if not sent_path.exists():
30
+ return False
31
+
32
+ subject_lower = subject.lower()
33
+ to_lower = to_addr.lower()
34
+ cutoff = datetime.now().timestamp() - (since_hours * 3600)
35
+ cur_dir = sent_path / 'cur'
36
+ if not cur_dir.exists():
37
+ return False
38
+
39
+ for msg_file in cur_dir.iterdir():
40
+ try:
41
+ if msg_file.stat().st_mtime < cutoff:
42
+ continue
43
+ content = msg_file.read_text(errors='ignore')
44
+ except (OSError, UnicodeDecodeError):
45
+ continue
46
+
47
+ content_lower = content.lower()
48
+ if f"to:{to_lower}" in content_lower or f"to: {to_lower}" in content_lower:
49
+ if subject_lower in content_lower:
50
+ return True
51
+ return False
52
+
53
+ def check_file_exists(self, pattern, search_dirs=None):
54
+ """Check if file matching pattern exists in common locations."""
55
+ if search_dirs is None:
56
+ search_dirs = [
57
+ '/var/www/vhosts',
58
+ str(NEXO_HOME),
59
+ '/opt'
60
+ ]
61
+
62
+ for base_dir in search_dirs:
63
+ if not os.path.exists(base_dir):
64
+ continue
65
+ matches = []
66
+ try:
67
+ for root, _, files in os.walk(base_dir):
68
+ for filename in files:
69
+ if pattern in filename:
70
+ matches.append(str(Path(root) / filename))
71
+ if len(matches) >= 5:
72
+ return matches
73
+ except OSError:
74
+ continue
75
+ return []
76
+
77
+ def check_action_done(self, action_type, identifier, ttl_days=7):
78
+ """Check if action was already performed recently."""
79
+ action_file = self.state_dir / 'actions.json'
80
+
81
+ # Load existing actions
82
+ actions = {}
83
+ if action_file.exists():
84
+ with open(action_file) as f:
85
+ actions = json.load(f)
86
+
87
+ # Create action key
88
+ key = hashlib.md5(f"{action_type}:{identifier}".encode()).hexdigest()
89
+
90
+ # Check if exists and not expired
91
+ if key in actions:
92
+ action_time = datetime.fromisoformat(actions[key]['timestamp'])
93
+ age_days = (datetime.now() - action_time).days
94
+ if age_days < ttl_days:
95
+ return True, actions[key]
96
+
97
+ return False, None
98
+
99
+ def mark_action_done(self, action_type, identifier, metadata=None):
100
+ """Mark action as completed."""
101
+ action_file = self.state_dir / 'actions.json'
102
+
103
+ # Load existing actions
104
+ actions = {}
105
+ if action_file.exists():
106
+ with open(action_file) as f:
107
+ actions = json.load(f)
108
+
109
+ # Add new action
110
+ key = hashlib.md5(f"{action_type}:{identifier}".encode()).hexdigest()
111
+ actions[key] = {
112
+ 'type': action_type,
113
+ 'identifier': identifier,
114
+ 'timestamp': datetime.now().isoformat(),
115
+ 'metadata': metadata or {}
116
+ }
117
+
118
+ # Save
119
+ with open(action_file, 'w') as f:
120
+ json.dump(actions, f, indent=2)
121
+
122
+ return key
123
+
124
+ def smart_check(action_description: str, context: str = "") -> dict:
125
+ """Use Claude CLI to intelligently check if an action would be redundant.
126
+
127
+ Goes beyond simple file/hash checks — understands intent and context
128
+ to detect semantic duplicates (e.g., "send welcome email" vs
129
+ "email onboarding message" to same person).
130
+ """
131
+ checker = ContextChecker()
132
+
133
+ # Gather mechanical context first
134
+ state_file = checker.state_dir / 'actions.json'
135
+ recent_actions = {}
136
+ if state_file.exists():
137
+ try:
138
+ all_actions = json.loads(state_file.read_text())
139
+ cutoff = datetime.now().timestamp() - (7 * 86400)
140
+ for k, v in all_actions.items():
141
+ try:
142
+ ts = datetime.fromisoformat(v['timestamp']).timestamp()
143
+ if ts > cutoff:
144
+ recent_actions[k] = v
145
+ except (ValueError, KeyError):
146
+ pass
147
+ except Exception:
148
+ pass
149
+
150
+ if not CLAUDE_CLI.exists():
151
+ return {"redundant": False, "reason": "CLI unavailable, cannot smart-check"}
152
+
153
+ prompt = f"""You are a context deduplication engine for NEXO operations.
154
+
155
+ PROPOSED ACTION:
156
+ {action_description}
157
+
158
+ ADDITIONAL CONTEXT:
159
+ {context or "None"}
160
+
161
+ RECENT ACTIONS (last 7 days):
162
+ {json.dumps(list(recent_actions.values()), indent=1, default=str)}
163
+
164
+ Respond with ONLY valid JSON (no markdown):
165
+ {{
166
+ "redundant": true/false,
167
+ "confidence": 0.0-1.0,
168
+ "reason": "<one line explanation>",
169
+ "matching_action": "<identifier of matching action if redundant, else null>"
170
+ }}
171
+
172
+ Rules:
173
+ - Same recipient + same intent within 72h = redundant
174
+ - Same file modification with same content = redundant
175
+ - Similar but different scope (e.g., different recipients) = NOT redundant
176
+ - When in doubt, say not redundant (false negatives are cheaper than false positives)"""
177
+ )
178
+ if auth_check.returncode != 0:
179
+ # CLI not authenticated, skip gracefully
180
+ return {"redundant": False, "reason": "CLI not authenticated — skipped analysis", "suggestion": "N/A"}
181
+
182
+ env = os.environ.copy()
183
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
184
+ env.pop("CLAUDECODE", None)
185
+ env.pop("CLAUDE_CODE", None)
186
+
187
+ try:
188
+ result = subprocess.run(
189
+ [str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text",
190
+ "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
191
+ capture_output=True, text=True, timeout=21600, env=env
192
+ )
193
+ if result.returncode == 0:
194
+ text = result.stdout.strip()
195
+ if "```json" in text:
196
+ text = text.split("```json")[1].split("```")[0]
197
+ elif "```" in text:
198
+ text = text.split("```")[1].split("```")[0]
199
+ return json.loads(text.strip())
200
+ except Exception:
201
+ pass
202
+
203
+ return {"redundant": False, "reason": "CLI check failed, defaulting to not redundant"}
204
+
205
+
206
+ def main():
207
+ """CLI interface for context checking."""
208
+ if len(sys.argv) < 3:
209
+ print("Usage: check-context.py <command> <args>")
210
+ print("Commands:")
211
+ print(" email <to> <subject> - Check if email was sent")
212
+ print(" file <pattern> - Check if file exists")
213
+ print(" action <type> <id> - Check if action was done")
214
+ print(" smart <description> [ctx] - Intelligent duplicate check via CLI")
215
+ sys.exit(1)
216
+
217
+ checker = ContextChecker()
218
+ command = sys.argv[1]
219
+
220
+ if command == 'email':
221
+ if len(sys.argv) < 4:
222
+ print("Usage: check-context.py email <to> <subject>")
223
+ sys.exit(1)
224
+ exists = checker.check_email_sent(sys.argv[2], sys.argv[3])
225
+ print("EXISTS" if exists else "NOT_FOUND")
226
+ sys.exit(0 if not exists else 1)
227
+
228
+ elif command == 'file':
229
+ files = checker.check_file_exists(sys.argv[2])
230
+ if files:
231
+ print("\n".join(files))
232
+ sys.exit(1)
233
+ else:
234
+ print("NOT_FOUND")
235
+ sys.exit(0)
236
+
237
+ elif command == 'action':
238
+ if len(sys.argv) < 4:
239
+ print("Usage: check-context.py action <type> <id>")
240
+ sys.exit(1)
241
+ done, data = checker.check_action_done(sys.argv[2], sys.argv[3])
242
+ if done:
243
+ print(f"DONE: {data}")
244
+ sys.exit(1)
245
+ else:
246
+ print("NOT_DONE")
247
+ sys.exit(0)
248
+
249
+ elif command == 'smart':
250
+ if len(sys.argv) < 3:
251
+ print("Usage: check-context.py smart <description> [context]")
252
+ sys.exit(1)
253
+ description = sys.argv[2]
254
+ context = sys.argv[3] if len(sys.argv) > 3 else ""
255
+ result = smart_check(description, context)
256
+ print(json.dumps(result, indent=2))
257
+ sys.exit(1 if result.get("redundant") else 0)
258
+
259
+ else:
260
+ print(f"Unknown command: {command}")
261
+ sys.exit(1)
262
+
263
+ if __name__ == '__main__':
264
+ main()