nexo-brain 1.7.0 → 2.1.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 (261) hide show
  1. package/README.md +160 -60
  2. package/bin/nexo-brain.js +680 -381
  3. package/package.json +18 -3
  4. package/scripts/migrate-to-unified.sh +813 -0
  5. package/scripts/migrate-v1.7-to-v1.8.py +214 -0
  6. package/scripts/pre-commit-check.sh +1 -1
  7. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  8. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  9. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  10. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  11. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  12. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  13. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  14. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  15. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  16. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  17. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  18. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  19. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  20. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  21. package/src/auto_close_sessions.py +1 -1
  22. package/src/auto_update.py +634 -0
  23. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  24. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  25. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  26. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  27. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  28. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  29. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  30. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  31. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  32. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  33. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  34. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  35. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  36. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  37. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  38. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  39. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  40. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  41. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  42. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  43. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  44. package/src/cognitive/_core.py +7 -3
  45. package/src/cognitive/_decay.py +1 -1
  46. package/src/cognitive/_search.py +1 -0
  47. package/src/cognitive/_trust.py +3 -3
  48. package/src/crons/manifest.json +106 -0
  49. package/src/crons/sync.py +217 -0
  50. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  51. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  52. package/src/dashboard/app.py +24 -4
  53. package/src/dashboard/templates/dashboard.html +3 -2
  54. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  55. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  56. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  57. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  58. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  59. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  60. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  61. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  62. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  63. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  64. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  65. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  66. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  67. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  68. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  69. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  70. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  71. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  72. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  73. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  74. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  75. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  76. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  77. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  78. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  79. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  80. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  81. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  82. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  83. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  84. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  85. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  86. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  87. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  90. package/src/db/_core.py +5 -1
  91. package/src/db/_episodic.py +2 -4
  92. package/src/db/_reminders.py +45 -6
  93. package/src/db/_schema.py +31 -0
  94. package/src/evolution_cycle.py +33 -11
  95. package/src/hooks/auto_capture.py +1 -1
  96. package/src/hooks/capture-tool-logs.sh +76 -0
  97. package/src/hooks/inbox-hook.sh +2 -1
  98. package/src/hooks/post-compact.sh +2 -1
  99. package/src/hooks/pre-compact.sh +104 -2
  100. package/src/hooks/session-start.sh +6 -2
  101. package/src/hooks/session-stop.sh +4 -2
  102. package/src/kg_populate.py +4 -1
  103. package/src/migrate_embeddings.py +4 -1
  104. package/src/plugin_loader.py +100 -34
  105. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  106. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  107. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  108. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  109. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  110. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  111. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  112. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  113. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  114. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  115. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  116. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  117. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  118. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  119. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  120. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  121. package/src/plugins/agents.py +2 -2
  122. package/src/plugins/backup.py +5 -4
  123. package/src/plugins/core_rules.py +37 -16
  124. package/src/plugins/episodic_memory.py +14 -5
  125. package/src/plugins/evolution.py +6 -2
  126. package/src/plugins/guard.py +20 -11
  127. package/src/plugins/update.py +256 -0
  128. package/src/requirements.txt +12 -0
  129. package/src/scripts/check-context.py +8 -3
  130. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  131. package/src/scripts/deep-sleep/apply_findings.py +514 -169
  132. package/src/scripts/deep-sleep/collect.py +480 -0
  133. package/src/scripts/deep-sleep/extract-prompt.md +233 -0
  134. package/src/scripts/deep-sleep/extract.py +249 -0
  135. package/src/scripts/deep-sleep/synthesize-prompt.md +168 -0
  136. package/src/scripts/deep-sleep/synthesize.py +191 -0
  137. package/src/scripts/nexo-auto-update.py +4 -211
  138. package/src/scripts/nexo-backup.sh +5 -13
  139. package/src/scripts/nexo-brain-activation.sh +26 -26
  140. package/src/scripts/nexo-catchup.py +36 -25
  141. package/src/scripts/nexo-cognitive-decay.py +7 -3
  142. package/src/scripts/nexo-daily-self-audit.py +44 -15
  143. package/src/scripts/nexo-deep-sleep.sh +31 -16
  144. package/src/scripts/nexo-evolution-run.py +21 -11
  145. package/src/scripts/nexo-followup-hygiene.py +6 -4
  146. package/src/scripts/nexo-github-monitor.py +12 -8
  147. package/src/scripts/nexo-immune.py +6 -4
  148. package/src/scripts/nexo-inbox-hook.sh +2 -1
  149. package/src/scripts/nexo-install.py +4 -225
  150. package/src/scripts/nexo-learning-housekeep.py +7 -3
  151. package/src/scripts/nexo-learning-validator.py +1 -22
  152. package/src/scripts/nexo-migrate.py +9 -3
  153. package/src/scripts/nexo-postmortem-consolidator.py +17 -10
  154. package/src/scripts/nexo-pre-commit.py +3 -1
  155. package/src/scripts/nexo-prevent-sleep.sh +29 -0
  156. package/src/scripts/nexo-proactive-dashboard.py +5 -4
  157. package/src/scripts/nexo-runtime-preflight.py +59 -55
  158. package/src/scripts/nexo-send-email.py +1 -1
  159. package/src/scripts/nexo-send-reply.py +3 -1
  160. package/src/scripts/nexo-sleep.py +13 -9
  161. package/src/scripts/nexo-snapshot-restore.sh +2 -1
  162. package/src/scripts/nexo-synthesis.py +11 -7
  163. package/src/scripts/nexo-tcc-approve.sh +79 -0
  164. package/src/scripts/nexo-update.sh +161 -0
  165. package/src/scripts/nexo-watchdog-smoke.py +18 -13
  166. package/src/scripts/nexo-watchdog.sh +22 -13
  167. package/src/server.py +77 -28
  168. package/src/storage_router.py +6 -2
  169. package/src/tools_learnings.py +6 -6
  170. package/src/tools_menu.py +1 -1
  171. package/src/tools_reminders_crud.py +10 -8
  172. package/src/tools_sessions.py +76 -4
  173. package/templates/CLAUDE.md.template +14 -80
  174. package/templates/launchagents/README.md +7 -7
  175. package/templates/launchagents/com.nexo.auto-close-sessions.plist +5 -1
  176. package/templates/launchagents/com.nexo.catchup.plist +4 -0
  177. package/templates/launchagents/com.nexo.cognitive-decay.plist +7 -0
  178. package/templates/launchagents/com.nexo.dashboard.plist +5 -1
  179. package/templates/launchagents/com.nexo.deep-sleep.plist +4 -0
  180. package/templates/launchagents/com.nexo.evolution.plist +4 -0
  181. package/templates/launchagents/com.nexo.followup-hygiene.plist +4 -0
  182. package/templates/launchagents/com.nexo.github-monitor.plist +3 -1
  183. package/templates/launchagents/com.nexo.immune.plist +4 -0
  184. package/templates/launchagents/com.nexo.postmortem.plist +4 -0
  185. package/templates/launchagents/com.nexo.self-audit.plist +4 -0
  186. package/templates/launchagents/com.nexo.synthesis.plist +4 -0
  187. package/templates/launchagents/com.nexo.watchdog.plist +4 -0
  188. package/templates/openclaw.json +1 -1
  189. package/tests/conftest.py +2 -2
  190. package/tests/test_cognitive.py +7 -6
  191. package/tests/test_migrations.py +26 -0
  192. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  193. package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
  194. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  195. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  196. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  197. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  198. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  199. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  200. package/src/__pycache__/server.cpython-314.pyc +0 -0
  201. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  202. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  203. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  204. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  205. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  206. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  207. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  208. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  209. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  210. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  211. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  212. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  213. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  214. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  215. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  216. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  217. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  218. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  219. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  220. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  221. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  222. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  223. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  224. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  225. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  226. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  227. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  232. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  233. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  234. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  235. package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
  236. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  237. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  238. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  239. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  240. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  241. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  242. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  243. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  244. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  245. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  246. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  247. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  248. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  249. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  250. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  251. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
  252. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
  253. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
  254. package/src/scripts/deep-sleep/analyze_session.py +0 -217
  255. package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
  256. package/src/scripts/deep-sleep/prompt.md +0 -109
  257. package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  258. package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  259. package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
  260. package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
  261. package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Deep Sleep v2 -- Phase 3: Synthesize extractions into actionable findings.
4
+
5
+ One Claude call that reads all per-session extractions and produces a
6
+ unified synthesis with cross-session patterns, morning agenda, context
7
+ packets, and deduplicated actions.
8
+
9
+ Environment variables:
10
+ NEXO_HOME -- root of the NEXO installation (default: ~/.nexo)
11
+ """
12
+ import json
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+
20
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
21
+ DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
22
+ PROMPT_FILE = Path(__file__).parent / "synthesize-prompt.md"
23
+
24
+ CLAUDE_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
25
+
26
+
27
+ def find_claude_cli() -> str:
28
+ """Find the Claude CLI binary."""
29
+ candidates = [
30
+ Path.home() / ".local" / "bin" / "claude",
31
+ Path("/usr/local/bin/claude"),
32
+ ]
33
+ for c in candidates:
34
+ if c.exists():
35
+ return str(c)
36
+ which = shutil.which("claude")
37
+ if which:
38
+ return which
39
+ return "claude"
40
+
41
+
42
+ def extract_json_from_response(text: str) -> dict | None:
43
+ """Parse JSON from Claude's response, handling markdown fences."""
44
+ text = text.strip()
45
+
46
+ if text.startswith("```"):
47
+ lines = text.split("\n")
48
+ end = len(lines)
49
+ for i in range(len(lines) - 1, 0, -1):
50
+ if lines[i].strip() == "```":
51
+ end = i
52
+ break
53
+ text = "\n".join(lines[1:end]).strip()
54
+
55
+ brace_start = text.find("{")
56
+ if brace_start < 0:
57
+ return None
58
+
59
+ depth = 0
60
+ for i in range(brace_start, len(text)):
61
+ if text[i] == "{":
62
+ depth += 1
63
+ elif text[i] == "}":
64
+ depth -= 1
65
+ if depth == 0:
66
+ try:
67
+ return json.loads(text[brace_start:i + 1])
68
+ except json.JSONDecodeError:
69
+ break
70
+ return None
71
+
72
+
73
+ def main():
74
+ target_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
75
+
76
+ extractions_file = DEEP_SLEEP_DIR / f"{target_date}-extractions.json"
77
+ context_file = DEEP_SLEEP_DIR / f"{target_date}-context.txt"
78
+
79
+ if not extractions_file.exists():
80
+ print(f"[synthesize] No extractions file for {target_date}. Run extract.py first.")
81
+ sys.exit(1)
82
+
83
+ # Check if there are any findings worth synthesizing
84
+ with open(extractions_file) as f:
85
+ extractions = json.load(f)
86
+
87
+ total_findings = extractions.get("total_findings", 0)
88
+ if total_findings == 0:
89
+ print(f"[synthesize] No findings to synthesize for {target_date}.")
90
+ # Write minimal synthesis
91
+ output = {
92
+ "date": target_date,
93
+ "sessions_analyzed": extractions.get("sessions_analyzed", 0),
94
+ "cross_session_patterns": [],
95
+ "morning_agenda": [],
96
+ "context_packets": [],
97
+ "actions": [],
98
+ "summary": f"No significant findings for {target_date}."
99
+ }
100
+ output_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
101
+ with open(output_file, "w") as f:
102
+ json.dump(output, f, indent=2, ensure_ascii=False)
103
+ print(f"[synthesize] Output: {output_file}")
104
+ return
105
+
106
+ # Build prompt
107
+ prompt_template = PROMPT_FILE.read_text()
108
+ prompt = prompt_template.replace("{{EXTRACTIONS_FILE}}", str(extractions_file))
109
+ prompt = prompt.replace("{{CONTEXT_FILE}}", str(context_file))
110
+
111
+ claude_bin = find_claude_cli()
112
+ print(f"[synthesize] Phase 3: Synthesizing {total_findings} findings from {target_date}")
113
+ print(f"[synthesize] Claude CLI: {claude_bin}")
114
+
115
+ try:
116
+ env = os.environ.copy()
117
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
118
+
119
+ result = subprocess.run(
120
+ [
121
+ claude_bin,
122
+ "-p", prompt,
123
+ "--model", "opus",
124
+ "--output-format", "text",
125
+ "--allowedTools",
126
+ "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__nexo_startup,mcp__nexo__nexo_learning_search,mcp__nexo__nexo_recall,mcp__nexo__nexo_reminders"
127
+ ],
128
+ capture_output=True,
129
+ text=True,
130
+ timeout=CLAUDE_TIMEOUT,
131
+ env=env
132
+ )
133
+
134
+ if result.returncode != 0:
135
+ print(f"[synthesize] Claude CLI error (exit {result.returncode}): {result.stderr[:300]}", file=sys.stderr)
136
+ sys.exit(1)
137
+
138
+ # Filter hook contamination
139
+ output_text = "\n".join(
140
+ l for l in result.stdout.strip().splitlines()
141
+ if not l.strip().startswith("Post-mortem")
142
+ )
143
+ parsed = extract_json_from_response(output_text)
144
+
145
+ # Fallback: Opus might have written the file directly via Write tool
146
+ if not parsed:
147
+ for candidate in [
148
+ DEEP_SLEEP_DIR / f"{target_date}-analysis.json",
149
+ DEEP_SLEEP_DIR / f"{target_date}-synthesis.json",
150
+ ]:
151
+ if candidate.exists() and candidate.stat().st_size > 100:
152
+ try:
153
+ parsed = json.load(open(candidate))
154
+ print(f"[synthesize] Opus wrote file directly: {candidate}")
155
+ break
156
+ except Exception:
157
+ continue
158
+
159
+ if not parsed:
160
+ debug_file = DEEP_SLEEP_DIR / f"debug-synthesize-{target_date}.txt"
161
+ debug_file.write_text(result.stdout[:10000])
162
+ print(f"[synthesize] Failed to parse JSON. Raw output saved to {debug_file}", file=sys.stderr)
163
+ sys.exit(1)
164
+
165
+ # Write synthesis output
166
+ output_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
167
+ with open(output_file, "w") as f:
168
+ json.dump(parsed, f, indent=2, ensure_ascii=False)
169
+
170
+ n_actions = len(parsed.get("actions", []))
171
+ n_patterns = len(parsed.get("cross_session_patterns", []))
172
+ n_agenda = len(parsed.get("morning_agenda", []))
173
+ n_packets = len(parsed.get("context_packets", []))
174
+
175
+ print(f"[synthesize] Done.")
176
+ print(f" Actions: {n_actions}")
177
+ print(f" Cross-session patterns: {n_patterns}")
178
+ print(f" Morning agenda items: {n_agenda}")
179
+ print(f" Context packets: {n_packets}")
180
+ print(f"[synthesize] Output: {output_file}")
181
+
182
+ except subprocess.TimeoutExpired:
183
+ print(f"[synthesize] Claude CLI timeout ({CLAUDE_TIMEOUT}s)", file=sys.stderr)
184
+ sys.exit(1)
185
+ except FileNotFoundError:
186
+ print(f"[synthesize] Claude CLI not found at: {claude_bin}", file=sys.stderr)
187
+ sys.exit(1)
188
+
189
+
190
+ if __name__ == "__main__":
191
+ main()
@@ -1,213 +1,6 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- NEXO Auto-Update — checks for new versions and applies updates.
4
-
5
- Runs at boot via the catch-up system or manually.
6
- Compares local version with the latest GitHub release.
7
- If a new version is available, downloads and applies the update.
8
- """
9
-
10
- import json
11
- import os
12
- import shutil
13
- import subprocess
2
+ """DEPRECATED: Updates are handled automatically by NEXO on startup."""
14
3
  import sys
15
- from datetime import datetime
16
- from pathlib import Path
17
-
18
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
19
- VERSION_FILE = NEXO_HOME / "version.json"
20
- LOG_FILE = NEXO_HOME / "logs" / "auto-update.log"
21
- REPO = "wazionapps/nexo"
22
-
23
-
24
- def log(msg: str):
25
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
26
- line = f"[{ts}] {msg}"
27
- print(line, flush=True)
28
- LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
29
- with open(LOG_FILE, "a") as f:
30
- f.write(line + "\n")
31
-
32
-
33
- def get_local_version() -> str:
34
- """Read locally installed version."""
35
- if VERSION_FILE.exists():
36
- try:
37
- return json.loads(VERSION_FILE.read_text()).get("version", "0.0.0")
38
- except Exception:
39
- pass
40
- return "0.0.0"
41
-
42
-
43
- def get_remote_version() -> dict | None:
44
- """Check latest release on GitHub."""
45
- try:
46
- result = subprocess.run(
47
- ["gh", "api", f"repos/{REPO}/releases/latest"],
48
- capture_output=True, text=True, timeout=15
49
- )
50
- if result.returncode == 0:
51
- data = json.loads(result.stdout)
52
- return {
53
- "version": data.get("tag_name", "").lstrip("v"),
54
- "tarball_url": data.get("tarball_url", ""),
55
- "published_at": data.get("published_at", ""),
56
- "body": data.get("body", "")[:500],
57
- }
58
- except Exception:
59
- pass
60
- return None
61
-
62
-
63
- def version_compare(local: str, remote: str) -> int:
64
- """Compare semver strings. Returns -1 (local older), 0 (same), 1 (local newer)."""
65
- def parse(v):
66
- parts = v.split(".")
67
- return tuple(int(p) for p in parts if p.isdigit())
68
-
69
- l, r = parse(local), parse(remote)
70
- if l < r:
71
- return -1
72
- elif l > r:
73
- return 1
74
- return 0
75
-
76
-
77
- def apply_update(tarball_url: str, new_version: str) -> bool:
78
- """Download and apply update from GitHub release tarball."""
79
- import tempfile
80
-
81
- log(f"Downloading update v{new_version}...")
82
- tmp_dir = Path(tempfile.mkdtemp(prefix="nexo-update-"))
83
-
84
- try:
85
- # Download tarball
86
- tarball = tmp_dir / "release.tar.gz"
87
- result = subprocess.run(
88
- ["curl", "-sL", "-o", str(tarball), tarball_url],
89
- capture_output=True, timeout=60
90
- )
91
- if result.returncode != 0:
92
- log(f"Download failed: {result.stderr}")
93
- return False
94
-
95
- # Extract
96
- subprocess.run(
97
- ["tar", "xzf", str(tarball), "-C", str(tmp_dir)],
98
- capture_output=True, timeout=30
99
- )
100
-
101
- # Find extracted directory (GitHub tarballs have a top-level dir)
102
- extracted = [d for d in tmp_dir.iterdir() if d.is_dir()]
103
- if not extracted:
104
- log("No directory found in tarball")
105
- return False
106
-
107
- src_dir = extracted[0] / "src"
108
- if not src_dir.exists():
109
- log("No src/ directory in release")
110
- return False
111
-
112
- # Backup current files
113
- backup_dir = NEXO_HOME / "backups" / f"pre-update-{new_version}"
114
- backup_dir.mkdir(parents=True, exist_ok=True)
115
-
116
- files_updated = 0
117
- # Update core Python files
118
- for py_file in src_dir.glob("*.py"):
119
- dest = NEXO_HOME / py_file.name
120
- if dest.exists():
121
- shutil.copy2(dest, backup_dir / py_file.name)
122
- shutil.copy2(py_file, dest)
123
- files_updated += 1
124
-
125
- # Update plugins
126
- plugins_src = src_dir / "plugins"
127
- if plugins_src.exists():
128
- plugins_dest = NEXO_HOME / "plugins"
129
- plugins_dest.mkdir(exist_ok=True)
130
- for py_file in plugins_src.glob("*.py"):
131
- dest = plugins_dest / py_file.name
132
- if dest.exists():
133
- shutil.copy2(dest, backup_dir / f"plugin_{py_file.name}")
134
- shutil.copy2(py_file, dest)
135
- files_updated += 1
136
-
137
- # Update scripts
138
- scripts_src = src_dir / "scripts"
139
- if scripts_src.exists():
140
- scripts_dest = NEXO_HOME / "scripts"
141
- scripts_dest.mkdir(exist_ok=True)
142
- for py_file in scripts_src.glob("*.py"):
143
- dest = scripts_dest / py_file.name
144
- if dest.exists():
145
- shutil.copy2(dest, backup_dir / f"script_{py_file.name}")
146
- shutil.copy2(py_file, dest)
147
- files_updated += 1
148
-
149
- # Update hooks
150
- hooks_src = src_dir / "hooks"
151
- if hooks_src.exists():
152
- hooks_dest = NEXO_HOME / "hooks"
153
- hooks_dest.mkdir(exist_ok=True)
154
- for sh_file in hooks_src.glob("*.sh"):
155
- dest = hooks_dest / sh_file.name
156
- shutil.copy2(sh_file, dest)
157
- os.chmod(dest, 0o755)
158
- files_updated += 1
159
-
160
- # Save new version
161
- VERSION_FILE.write_text(json.dumps({
162
- "version": new_version,
163
- "updated_at": datetime.now().isoformat(),
164
- "files_updated": files_updated,
165
- "backup": str(backup_dir),
166
- }, indent=2))
167
-
168
- log(f"Update applied: {files_updated} files updated. Backup at {backup_dir}")
169
- return True
170
-
171
- except Exception as e:
172
- log(f"Update failed: {e}")
173
- return False
174
- finally:
175
- shutil.rmtree(tmp_dir, ignore_errors=True)
176
-
177
-
178
- def main():
179
- log("=== NEXO Auto-Update check ===")
180
-
181
- local_ver = get_local_version()
182
- log(f"Local version: {local_ver}")
183
-
184
- remote = get_remote_version()
185
- if not remote:
186
- log("Could not check remote version (no network or no releases)")
187
- return
188
-
189
- remote_ver = remote["version"]
190
- log(f"Remote version: {remote_ver}")
191
-
192
- cmp = version_compare(local_ver, remote_ver)
193
- if cmp >= 0:
194
- log("Already up to date.")
195
- return
196
-
197
- log(f"Update available: {local_ver} → {remote_ver}")
198
- log(f"Release notes: {remote['body'][:200]}")
199
-
200
- if remote["tarball_url"]:
201
- success = apply_update(remote["tarball_url"], remote_ver)
202
- if success:
203
- log(f"Successfully updated to v{remote_ver}")
204
- else:
205
- log("Update failed — will retry next boot")
206
- else:
207
- log("No tarball URL in release — manual update needed")
208
-
209
- log("=== Done ===")
210
-
211
-
212
- if __name__ == "__main__":
213
- main()
4
+ print("This script is deprecated. NEXO auto-updates on startup.")
5
+ print("To update manually, use the nexo_update MCP tool.")
6
+ sys.exit(0)
@@ -1,20 +1,12 @@
1
1
  #!/bin/bash
2
- # NEXO DB hourly backup
3
- # Install: crontab -e → 0 * * * * /path/to/nexo-backup.sh
4
- #
5
- # Keeps hourly backups for 48h and weekly backups for 90 days.
6
-
7
- NEXO_DIR="${NEXO_DIR:-$(dirname "$(dirname "$(realpath "$0")")")}"
8
- BACKUP_DIR="$NEXO_DIR/backups"
2
+ # NEXO DB hourly backup — crontab: 0 * * * * $NEXO_HOME/scripts/nexo-backup.sh
3
+ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
4
+ NEXO_DIR="$NEXO_HOME"
5
+ BACKUP_DIR="$NEXO_HOME/backups"
9
6
  WEEKLY_DIR="$BACKUP_DIR/weekly"
10
- DB="$NEXO_DIR/db/nexo.db"
7
+ DB="$NEXO_HOME/data/nexo.db"
11
8
  RETENTION_HOURS=48
12
9
 
13
- if [ ! -f "$DB" ]; then
14
- echo "ERROR: nexo.db not found at $DB" >&2
15
- exit 1
16
- fi
17
-
18
10
  mkdir -p "$BACKUP_DIR" "$WEEKLY_DIR"
19
11
 
20
12
  # Hourly backup
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
- # nexo-brain-activation.sh — NF24: Activación Espontánea
3
- # Lee user_model.json, detecta shifts significativos y genera insights
4
- # para el arranque de NEXO. Si no hay nada relevante: exit 0 sin output.
2
+ # nexo-brain-activation.sh — NF24: Spontaneous Activation
3
+ # Reads user_model.json, detects significant shifts, and generates insights
4
+ # for NEXO startup. If nothing relevant: exit 0 with no output.
5
5
 
6
6
  set -euo pipefail
7
7
 
@@ -9,12 +9,12 @@ BRAIN_DIR="~/.nexo/brain"
9
9
  MODEL_FILE="$BRAIN_DIR/user_model.json"
10
10
  SUMMARIES_DIR="$BRAIN_DIR/daily_summaries"
11
11
 
12
- # --- Guardia: archivo imprescindible ---
12
+ # --- Guard: required file ---
13
13
  if [[ ! -f "$MODEL_FILE" ]]; then
14
14
  exit 0
15
15
  fi
16
16
 
17
- # --- Python inline para parsear JSON y analizar ---
17
+ # --- Inline Python to parse JSON and analyze ---
18
18
  python3 - <<'PYEOF'
19
19
  import json
20
20
  import sys
@@ -26,7 +26,7 @@ BRAIN_DIR = os.path.expanduser("~/.nexo/brain")
26
26
  MODEL_FILE = os.path.join(BRAIN_DIR, "user_model.json")
27
27
  SUMMARIES_DIR = os.path.join(BRAIN_DIR, "daily_summaries")
28
28
 
29
- # ── Cargar modelo ──────────────────────────────────────────────────────────
29
+ # ── Load model ──────────────────────────────────────────────────────────
30
30
  try:
31
31
  with open(MODEL_FILE) as f:
32
32
  model = json.load(f)
@@ -35,11 +35,11 @@ except Exception:
35
35
 
36
36
  insights = []
37
37
 
38
- # ── 1. Analizar evolution_log — últimas 3-5 entradas ─────────────────────
38
+ # ── 1. Analyze evolution_log — last 3-5 entries ─────────────────────
39
39
  evolution = model.get("evolution_log", [])
40
40
  recent = evolution[-5:] if len(evolution) >= 5 else evolution
41
41
 
42
- # Detectar entradas de los últimos 3 días
42
+ # Detect entries from the last 3 days
43
43
  today = datetime.now().date()
44
44
  cutoff = today - timedelta(days=3)
45
45
 
@@ -56,9 +56,9 @@ if recent_obs:
56
56
  for obs in recent_obs[-2:]: # max 2 most recent
57
57
  insights.append(obs["observation"].strip())
58
58
 
59
- # ── 2. Detectar cambios de traits >0.1 entre entradas (si hay histórico) ──
60
- # El modelo actual solo tiene snapshot actual; si en el futuro hay histórico
61
- # en evolution_log con trait deltas, se puede ampliar aquí.
59
+ # ── 2. Detect trait changes >0.1 between entries (if history exists) ──
60
+ # The current model only has a snapshot; if in the future there is history
61
+ # in evolution_log with trait deltas, this can be expanded here.
62
62
  # Detect the most extreme trait as the dominant identity signal.
63
63
  traits = model.get("traits", {})
64
64
  if traits:
@@ -66,13 +66,13 @@ if traits:
66
66
  dominant_val = traits[dominant]
67
67
  weakest = min(traits, key=lambda k: traits[k])
68
68
  weakest_val = traits[weakest]
69
- # Solo reportar si es muy extremo (>0.9 o <0.2) para no saturar
69
+ # Only report if extreme (>0.9 or <0.2) to avoid noise
70
70
  if dominant_val >= 0.9:
71
- insights.append(f"Trait dominante: {dominant}={dominant_val} (máximo registrado)")
71
+ insights.append(f"Dominant trait: {dominant}={dominant_val} (highest recorded)")
72
72
  if weakest_val <= 0.2:
73
- insights.append(f"Trait en mínimo: {weakest}={weakest_val} (baja tolerancia activa)")
73
+ insights.append(f"Lowest trait: {weakest}={weakest_val} (low tolerance active)")
74
74
 
75
- # ── 3. Contradicciones recientes (últimos 3 días) ─────────────────────────
75
+ # ── 3. Recent contradictions (last 3 days) ─────────────────────────
76
76
  contradictions = model.get("contradictions", [])
77
77
  recent_contradictions = []
78
78
  for c in contradictions:
@@ -84,24 +84,24 @@ for c in contradictions:
84
84
  pass
85
85
 
86
86
  for c in recent_contradictions:
87
- insights.append(f"Contradicción detectada: {c['description'].strip()}")
87
+ insights.append(f"Contradiction detected: {c['description'].strip()}")
88
88
 
89
- # ── 4. Foco activo actual ─────────────────────────────────────────────────
89
+ # ── 4. Current active focus ─────────────────────────────────────────────────
90
90
  current_focus = model.get("current_focus", [])
91
- # Solo incluir si hay focos (y no es el arranque inicial)
91
+ # Only include if there are focus items (and it's not initial startup)
92
92
  if len(current_focus) > 0 and len(insights) == 0:
93
- # Si no hay otros insights, reportar foco como contexto mínimo
93
+ # If no other insights, report focus as minimum context
94
94
  focus_str = ", ".join(current_focus)
95
- insights.append(f"Foco actual: {focus_str}")
95
+ insights.append(f"Current focus: {focus_str}")
96
96
 
97
- # ── 5. Goals: cambios active/dormant ─────────────────────────────────────
97
+ # ── 5. Goals: active/dormant changes ─────────────────────────────────────
98
98
  goals_active = model.get("goals_active", [])
99
99
  goals_dormant = model.get("goals_dormant", [])
100
- # Si hay goals dormant, mencionarlos (pueden necesitar reactivación)
100
+ # If there are dormant goals, mention them (may need reactivation)
101
101
  if goals_dormant:
102
102
  insights.append(f"Goal dormant: {goals_dormant[0]}")
103
103
 
104
- # ── Leer último daily summary ─────────────────────────────────────────────
104
+ # ── Read last daily summary ─────────────────────────────────────────────
105
105
  summary_text = ""
106
106
  try:
107
107
  files = sorted(glob.glob(os.path.join(SUMMARIES_DIR, "*.md")))
@@ -109,17 +109,17 @@ try:
109
109
  with open(files[-1]) as f:
110
110
  lines = [l.rstrip() for l in f.readlines() if l.strip()]
111
111
  # Take up to 3 lines of content (exclude header)
112
- content_lines = [l for l in lines if not l.startswith("# Resumen")]
112
+ content_lines = [l for l in lines if not l.startswith("# Resumen") and not l.startswith("# Summary")]
113
113
  summary_text = " | ".join(content_lines[:3])
114
114
  except Exception:
115
115
  pass
116
116
 
117
117
  # ── Output ────────────────────────────────────────────────────────────────
118
- # Si no hay insights reales, salir sin output
118
+ # If no real insights, exit without output
119
119
  if not insights and not summary_text:
120
120
  sys.exit(0)
121
121
 
122
- # Deduplicar y limitar
122
+ # Deduplicate and limit
123
123
  seen = set()
124
124
  unique_insights = []
125
125
  for ins in insights:
@@ -27,15 +27,32 @@ from pathlib import Path
27
27
  CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
28
28
 
29
29
  HOME = Path.home()
30
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
30
31
  LOG_DIR = NEXO_HOME / "logs"
31
32
  LOG_DIR.mkdir(parents=True, exist_ok=True)
32
33
  LOG_FILE = LOG_DIR / "catchup.log"
33
34
  STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
34
35
 
35
- PYTHON_BREW = "/opt/homebrew/bin/python3"
36
- PYTHON_SYS = "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3"
37
36
  SCRIPTS = NEXO_HOME / "scripts"
38
37
 
38
+ # Resolve Python: prefer NEXO's venv, then the same Python running this script
39
+ def _resolve_python() -> str:
40
+ """Find the best Python to use for subprocess calls."""
41
+ # Check for NEXO_CODE env var pointing to the repo's src/
42
+ nexo_code = os.environ.get("NEXO_CODE", "")
43
+ if nexo_code:
44
+ venv_python = Path(nexo_code).parent / ".venv" / "bin" / "python"
45
+ if venv_python.exists():
46
+ return str(venv_python)
47
+ # Check for venv relative to NEXO_HOME
48
+ venv_python = NEXO_HOME / ".venv" / "bin" / "python"
49
+ if venv_python.exists():
50
+ return str(venv_python)
51
+ # Fall back to the same Python running this script
52
+ return sys.executable
53
+
54
+ NEXO_PYTHON = _resolve_python()
55
+
39
56
 
40
57
  def log(msg: str):
41
58
  ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -108,7 +125,7 @@ def run_task(name: str, python: str, script: str, state: dict) -> bool:
108
125
  try:
109
126
  result = subprocess.run(
110
127
  [python, script_path],
111
- capture_output=True, text=True, timeout=300,
128
+ capture_output=True, text=True, timeout=21600,
112
129
  env={**os.environ, "HOME": str(HOME), "NEXO_CATCHUP": "1"}
113
130
  )
114
131
  if result.returncode == 0:
@@ -133,27 +150,15 @@ def main():
133
150
  state = load_state()
134
151
 
135
152
  # Define tasks in execution order (matching their intended schedule order)
136
- # Auto-update check FIRST
137
- update_script = SCRIPTS / "nexo-auto-update.py"
138
- if update_script.exists():
139
- log("Checking for NEXO updates...")
140
- try:
141
- subprocess.run(
142
- [PYTHON_BREW if os.path.exists(PYTHON_BREW) else PYTHON_SYS, str(update_script)],
143
- capture_output=True, text=True, timeout=60,
144
- env={**os.environ, "HOME": str(HOME), "NEXO_HOME": str(NEXO_HOME / "nexo-mcp")}
145
- )
146
- except Exception as e:
147
- log(f" Update check failed: {e}")
148
-
153
+ # Note: auto-update is handled by the MCP server on startup, not by catchup.
149
154
  tasks = [
150
155
  # (name, hour, minute, python, script, weekday)
151
- ("cognitive-decay", 3, 0, PYTHON_BREW, "nexo-cognitive-decay.py", None),
152
- ("evolution", 3, 0, PYTHON_SYS, "nexo-evolution-run.py", 6), # Sunday = 6
153
- ("sleep", 4, 0, PYTHON_SYS, "nexo-sleep.py", None),
154
- ("self-audit", 7, 0, PYTHON_SYS, "nexo-daily-self-audit.py", None),
155
- ("github-monitor", 8, 0, PYTHON_BREW, "nexo-github-monitor.py", None),
156
- ("postmortem", 23, 30, PYTHON_BREW, "nexo-postmortem-consolidator.py", None),
156
+ ("cognitive-decay", 3, 0, NEXO_PYTHON, "nexo-cognitive-decay.py", None),
157
+ ("evolution", 3, 0, NEXO_PYTHON, "nexo-evolution-run.py", 6), # Sunday = 6
158
+ ("sleep", 4, 0, NEXO_PYTHON, "nexo-sleep.py", None),
159
+ ("self-audit", 7, 0, NEXO_PYTHON, "nexo-daily-self-audit.py", None),
160
+ ("github-monitor", 8, 0, NEXO_PYTHON, "nexo-github-monitor.py", None),
161
+ ("postmortem", 23, 30, NEXO_PYTHON, "nexo-postmortem-consolidator.py", None),
157
162
  ]
158
163
 
159
164
  ran = 0
@@ -182,6 +187,11 @@ def _cli_post_catchup_assessment(ran: int, skipped: int, state: dict):
182
187
  if not CLAUDE_CLI.exists():
183
188
  log(f"Caught up {ran} tasks, {skipped} already current. (CLI unavailable for assessment)")
184
189
  return
190
+ )
191
+ if auth_check.returncode != 0:
192
+ # CLI not authenticated, skip gracefully
193
+ print(f"[{datetime.now().strftime('%H:%M:%S')}] Claude CLI not authenticated. Skipping CLI analysis.")
194
+ return
185
195
 
186
196
  assessment_file = LOG_DIR / "catchup-assessment.md"
187
197
  state_summary = json.dumps(state, indent=2, default=str)
@@ -208,14 +218,15 @@ Format:
208
218
 
209
219
  log(f"Caught up {ran} tasks — running CLI assessment...")
210
220
  env = os.environ.copy()
221
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
211
222
  env.pop("CLAUDECODE", None)
212
223
  env.pop("CLAUDE_CODE", None)
213
224
 
214
225
  try:
215
226
  result = subprocess.run(
216
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
217
- "--allowedTools", "Read,Write,Edit,Glob,Grep"],
218
- capture_output=True, text=True, timeout=90, env=env
227
+ [str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text",
228
+ "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
229
+ capture_output=True, text=True, timeout=21600, env=env
219
230
  )
220
231
  if result.returncode == 0:
221
232
  log(f"Assessment written to {assessment_file}")