nexo-brain 2.0.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 (238) hide show
  1. package/README.md +140 -41
  2. package/package.json +15 -3
  3. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  4. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  5. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  6. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  7. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  8. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  9. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  10. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  11. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  12. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  13. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  14. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  15. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  16. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  17. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  18. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  19. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  20. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  21. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  22. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  23. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  24. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  25. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  26. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  27. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  28. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  29. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  30. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  31. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  32. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  33. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  34. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  35. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  36. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  37. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  38. package/src/crons/manifest.json +106 -0
  39. package/src/crons/sync.py +217 -0
  40. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  41. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  42. package/src/dashboard/app.py +16 -2
  43. package/src/dashboard/templates/dashboard.html +3 -2
  44. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  45. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  47. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  48. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  49. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  50. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  51. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  52. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  53. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  54. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  55. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  56. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  57. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  58. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  59. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  60. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  61. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  62. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  63. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  64. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  65. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  66. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  67. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  68. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  69. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  70. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  71. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  72. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  73. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  74. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  75. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  76. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  77. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  78. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  79. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  80. package/src/db/_episodic.py +1 -1
  81. package/src/db/_reminders.py +9 -5
  82. package/src/hooks/session-stop.sh +2 -1
  83. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  84. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  85. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  86. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  87. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  88. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  89. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  90. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  91. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  92. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  93. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  94. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  95. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  96. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  97. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  98. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  99. package/src/plugins/core_rules.py +34 -17
  100. package/src/plugins/update.py +18 -0
  101. package/src/scripts/check-context.py +4 -7
  102. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  103. package/src/scripts/deep-sleep/apply_findings.py +512 -167
  104. package/src/scripts/deep-sleep/collect.py +480 -0
  105. package/src/scripts/deep-sleep/extract-prompt.md +233 -0
  106. package/src/scripts/deep-sleep/extract.py +249 -0
  107. package/src/scripts/deep-sleep/synthesize-prompt.md +168 -0
  108. package/src/scripts/deep-sleep/synthesize.py +191 -0
  109. package/src/scripts/nexo-catchup.py +5 -8
  110. package/src/scripts/nexo-daily-self-audit.py +28 -19
  111. package/src/scripts/nexo-deep-sleep.sh +31 -16
  112. package/src/scripts/nexo-evolution-run.py +5 -20
  113. package/src/scripts/nexo-followup-hygiene.py +4 -2
  114. package/src/scripts/nexo-github-monitor.py +6 -9
  115. package/src/scripts/nexo-immune.py +4 -17
  116. package/src/scripts/nexo-learning-validator.py +0 -29
  117. package/src/scripts/nexo-postmortem-consolidator.py +9 -20
  118. package/src/scripts/nexo-proactive-dashboard.py +1 -0
  119. package/src/scripts/nexo-sleep.py +8 -18
  120. package/src/scripts/nexo-synthesis.py +8 -19
  121. package/src/tools_menu.py +1 -1
  122. package/src/tools_sessions.py +67 -0
  123. package/src/__pycache__/auto_close_sessions.cpython-310.pyc +0 -0
  124. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  125. package/src/__pycache__/auto_update.cpython-314.pyc +0 -0
  126. package/src/__pycache__/claim_graph.cpython-310.pyc +0 -0
  127. package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
  128. package/src/__pycache__/evolution_cycle.cpython-310.pyc +0 -0
  129. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  130. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  131. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  132. package/src/__pycache__/maintenance.cpython-310.pyc +0 -0
  133. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  134. package/src/__pycache__/migrate_embeddings.cpython-310.pyc +0 -0
  135. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  136. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  137. package/src/__pycache__/server.cpython-310.pyc +0 -0
  138. package/src/__pycache__/server.cpython-314.pyc +0 -0
  139. package/src/__pycache__/storage_router.cpython-310.pyc +0 -0
  140. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  141. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  142. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  143. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  144. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  145. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  146. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  147. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  148. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  149. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  150. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  151. package/src/hooks/__pycache__/auto_capture.cpython-310.pyc +0 -0
  152. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  154. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  155. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  156. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  157. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  158. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  159. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  160. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  161. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  162. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  163. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  164. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  165. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  166. package/src/rules/__pycache__/__init__.cpython-310.pyc +0 -0
  167. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  168. package/src/rules/__pycache__/migrate.cpython-310.pyc +0 -0
  169. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  170. package/src/scripts/__pycache__/check-context.cpython-310.pyc +0 -0
  171. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  172. package/src/scripts/__pycache__/nexo-auto-update.cpython-310.pyc +0 -0
  173. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  174. package/src/scripts/__pycache__/nexo-catchup.cpython-310.pyc +0 -0
  175. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  176. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-310.pyc +0 -0
  177. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  178. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-310.pyc +0 -0
  179. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  180. package/src/scripts/__pycache__/nexo-evolution-run.cpython-310.pyc +0 -0
  181. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  182. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-310.pyc +0 -0
  183. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  184. package/src/scripts/__pycache__/nexo-github-monitor.cpython-310.pyc +0 -0
  185. package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
  186. package/src/scripts/__pycache__/nexo-immune.cpython-310.pyc +0 -0
  187. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  188. package/src/scripts/__pycache__/nexo-install.cpython-310.pyc +0 -0
  189. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  190. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-310.pyc +0 -0
  191. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  192. package/src/scripts/__pycache__/nexo-learning-validator.cpython-310.pyc +0 -0
  193. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  194. package/src/scripts/__pycache__/nexo-migrate.cpython-310.pyc +0 -0
  195. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  196. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-310.pyc +0 -0
  197. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  198. package/src/scripts/__pycache__/nexo-pre-commit.cpython-310.pyc +0 -0
  199. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  200. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-310.pyc +0 -0
  201. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  202. package/src/scripts/__pycache__/nexo-reflection.cpython-310.pyc +0 -0
  203. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  204. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-310.pyc +0 -0
  205. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  206. package/src/scripts/__pycache__/nexo-send-email.cpython-310.pyc +0 -0
  207. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  208. package/src/scripts/__pycache__/nexo-send-reply.cpython-310.pyc +0 -0
  209. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  210. package/src/scripts/__pycache__/nexo-sleep.cpython-310.pyc +0 -0
  211. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-synthesis.cpython-310.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-310.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  216. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-310.pyc +0 -0
  217. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
  218. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-310.pyc +0 -0
  219. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
  220. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-310.pyc +0 -0
  221. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
  222. package/src/scripts/deep-sleep/analyze_session.py +0 -217
  223. package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
  224. package/src/scripts/deep-sleep/prompt.md +0 -109
  225. package/tests/__pycache__/__init__.cpython-310.pyc +0 -0
  226. package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  227. package/tests/__pycache__/conftest.cpython-310-pytest-9.0.2.pyc +0 -0
  228. package/tests/__pycache__/conftest.cpython-310.pyc +0 -0
  229. package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  230. package/tests/__pycache__/test_cognitive.cpython-310-pytest-9.0.2.pyc +0 -0
  231. package/tests/__pycache__/test_cognitive.cpython-310.pyc +0 -0
  232. package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
  233. package/tests/__pycache__/test_knowledge_graph.cpython-310-pytest-9.0.2.pyc +0 -0
  234. package/tests/__pycache__/test_knowledge_graph.cpython-310.pyc +0 -0
  235. package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
  236. package/tests/__pycache__/test_migrations.cpython-310-pytest-9.0.2.pyc +0 -0
  237. package/tests/__pycache__/test_migrations.cpython-310.pyc +0 -0
  238. 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()
@@ -125,7 +125,7 @@ def run_task(name: str, python: str, script: str, state: dict) -> bool:
125
125
  try:
126
126
  result = subprocess.run(
127
127
  [python, script_path],
128
- capture_output=True, text=True, timeout=300,
128
+ capture_output=True, text=True, timeout=21600,
129
129
  env={**os.environ, "HOME": str(HOME), "NEXO_CATCHUP": "1"}
130
130
  )
131
131
  if result.returncode == 0:
@@ -187,10 +187,6 @@ def _cli_post_catchup_assessment(ran: int, skipped: int, state: dict):
187
187
  if not CLAUDE_CLI.exists():
188
188
  log(f"Caught up {ran} tasks, {skipped} already current. (CLI unavailable for assessment)")
189
189
  return
190
-
191
- auth_check = subprocess.run(
192
- [str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
193
- capture_output=True, text=True, timeout=15
194
190
  )
195
191
  if auth_check.returncode != 0:
196
192
  # CLI not authenticated, skip gracefully
@@ -222,14 +218,15 @@ Format:
222
218
 
223
219
  log(f"Caught up {ran} tasks — running CLI assessment...")
224
220
  env = os.environ.copy()
221
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
225
222
  env.pop("CLAUDECODE", None)
226
223
  env.pop("CLAUDE_CODE", None)
227
224
 
228
225
  try:
229
226
  result = subprocess.run(
230
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text", "--bare",
231
- "--allowedTools", "Read,Write,Edit,Glob,Grep"],
232
- 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
233
230
  )
234
231
  if result.returncode == 0:
235
232
  log(f"Assessment written to {assessment_file}")
@@ -303,6 +303,25 @@ def check_runtime_preflight():
303
303
  finding("ERROR", "preflight", "runtime preflight failing")
304
304
 
305
305
 
306
+ def run_watchdog_smoke():
307
+ """Run the watchdog smoke test so its summary is fresh before we check it."""
308
+ smoke_script = Path(__file__).resolve().parent / "nexo-watchdog-smoke.py"
309
+ if not smoke_script.exists():
310
+ finding("WARN", "watchdog", f"smoke script not found at {smoke_script}")
311
+ return
312
+ try:
313
+ result = subprocess.run(
314
+ [sys.executable, str(smoke_script)],
315
+ capture_output=True, text=True, timeout=60
316
+ )
317
+ if result.returncode != 0:
318
+ finding("WARN", "watchdog", f"smoke test exited {result.returncode}")
319
+ except subprocess.TimeoutExpired:
320
+ finding("ERROR", "watchdog", "smoke test timed out (60s)")
321
+ except Exception as e:
322
+ finding("WARN", "watchdog", f"smoke test failed: {e}")
323
+
324
+
306
325
  def check_watchdog_smoke():
307
326
  if not WATCHDOG_SMOKE_SUMMARY.exists():
308
327
  return
@@ -409,9 +428,11 @@ def interpret_findings(raw_findings: list) -> bool:
409
428
 
410
429
  findings_json = json.dumps(raw_findings, ensure_ascii=False, indent=1)
411
430
 
412
- prompt = f"""You are NEXO's morning self-audit interpreter. The mechanical checks found
431
+ prompt = f"""FIRST: Call nexo_startup(task='daily self-audit') to register this session.
432
+
433
+ You are NEXO's morning self-audit interpreter. The mechanical checks found
413
434
  {len(errors)} errors and {len(warns)} warnings. Your job is to UNDERSTAND what's
414
- actually wrong, not just list findings.
435
+ actually wrong, not just list findings. Use nexo_learning_add for new findings and nexo_followup_create for action items.
415
436
 
416
437
  RAW FINDINGS:
417
438
  {findings_json}
@@ -441,30 +462,17 @@ Also write the machine-readable summary to {LOG_DIR}/self-audit-summary.json.
441
462
  Execute without asking."""
442
463
 
443
464
  log("Stage B: Invoking Claude CLI (opus) for interpretation...")
444
-
445
- # Verify Claude CLI is authenticated before calling
446
- try:
447
- auth_check = subprocess.run(
448
- [str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
449
- capture_output=True, text=True, timeout=15
450
- )
451
- if auth_check.returncode != 0:
452
- log("Stage B: Claude CLI not available or not authenticated. Skipping Stage B.")
453
- return False
454
- except Exception:
455
- log("Stage B: Claude CLI check failed. Skipping Stage B.")
456
- return False
457
-
458
465
  env = os.environ.copy()
466
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
459
467
  env.pop("CLAUDECODE", None)
460
468
  env.pop("CLAUDE_CODE", None)
461
469
 
462
470
  try:
463
471
  result = subprocess.run(
464
472
  [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
465
- "--output-format", "text", "--bare",
466
- "--allowedTools", "Read,Write,Edit,Glob,Grep"],
467
- capture_output=True, text=True, timeout=180, env=env
473
+ "--output-format", "text",
474
+ "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
475
+ capture_output=True, text=True, timeout=21600, env=env
468
476
  )
469
477
 
470
478
  if result.returncode != 0:
@@ -507,6 +515,7 @@ def main():
507
515
  check_restore_activity()
508
516
  check_bad_responses()
509
517
  check_runtime_preflight()
518
+ run_watchdog_smoke()
510
519
  check_watchdog_smoke()
511
520
  check_cognitive_health()
512
521
 
@@ -24,38 +24,53 @@ log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_DIR/deep-sleep.l
24
24
 
25
25
  run_analysis() {
26
26
  local DATE="$1"
27
- log "=== Deep Sleep starting for $DATE ==="
27
+ log "=== Deep Sleep v2 starting for $DATE ==="
28
28
 
29
- # Step 1: Collect transcripts
30
- log "Step 1: Collecting transcripts for $DATE..."
31
- python3 "$SCRIPT_DIR/deep-sleep/collect_transcripts.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
29
+ # Phase 1: Collect all context (Python, no LLM)
30
+ log "Phase 1: Collecting context for $DATE..."
31
+ python3 "$SCRIPT_DIR/deep-sleep/collect.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
32
32
 
33
- # Check if transcripts were found
34
- if [ ! -f "$DEEP_SLEEP_DIR/$DATE-transcripts.json" ]; then
35
- log "No transcripts file generated for $DATE. Skipping."
33
+ if [ ! -f "$DEEP_SLEEP_DIR/$DATE-context.txt" ]; then
34
+ log "No context file generated for $DATE. Skipping."
36
35
  return 0
37
36
  fi
38
37
 
39
- SESSIONS=$(python3 -c "import json; print(json.load(open('$DEEP_SLEEP_DIR/$DATE-transcripts.json'))['sessions_found'])")
38
+ # Check meta for session count
39
+ SESSIONS=0
40
+ if [ -f "$DEEP_SLEEP_DIR/$DATE-meta.json" ]; then
41
+ SESSIONS=$(python3 -c "import json; print(json.load(open('$DEEP_SLEEP_DIR/$DATE-meta.json'))['sessions_found'])")
42
+ elif [ -f "$DEEP_SLEEP_DIR/$DATE-index.json" ]; then
43
+ SESSIONS=$(python3 -c "import json; print(json.load(open('$DEEP_SLEEP_DIR/$DATE-index.json'))['sessions_found'])")
44
+ fi
40
45
  if [ "$SESSIONS" -eq 0 ]; then
41
46
  log "No sessions found for $DATE. Skipping."
42
47
  return 0
43
48
  fi
44
49
 
45
- # Step 2: Analyze with Claude CLI
46
- log "Step 2: Analyzing $SESSIONS sessions with Claude CLI..."
47
- python3 "$SCRIPT_DIR/deep-sleep/analyze_session.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
50
+ # Phase 2: Extract findings per session (Claude Opus)
51
+ log "Phase 2: Extracting findings from $SESSIONS sessions..."
52
+ python3 "$SCRIPT_DIR/deep-sleep/extract.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
48
53
 
49
- if [ ! -f "$DEEP_SLEEP_DIR/$DATE-analysis.json" ]; then
50
- log "Analysis failed for $DATE. No output generated."
54
+ if [ ! -f "$DEEP_SLEEP_DIR/$DATE-extractions.json" ]; then
55
+ log "Extraction failed for $DATE. No output."
51
56
  return 1
52
57
  fi
53
58
 
54
- # Step 3: Apply findings
55
- log "Step 3: Applying findings for $DATE..."
59
+ # Phase 3: Cross-session synthesis (Claude Opus, one call)
60
+ log "Phase 3: Synthesizing cross-session findings..."
61
+ python3 "$SCRIPT_DIR/deep-sleep/synthesize.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
62
+
63
+ if [ ! -f "$DEEP_SLEEP_DIR/$DATE-synthesis.json" ]; then
64
+ log "Synthesis failed for $DATE. Falling back to extractions only."
65
+ # Fall back: apply extractions directly
66
+ cp "$DEEP_SLEEP_DIR/$DATE-extractions.json" "$DEEP_SLEEP_DIR/$DATE-synthesis.json"
67
+ fi
68
+
69
+ # Phase 4: Apply findings
70
+ log "Phase 4: Applying findings..."
56
71
  python3 "$SCRIPT_DIR/deep-sleep/apply_findings.py" "$DATE" 2>&1 | tee -a "$LOG_DIR/deep-sleep.log"
57
72
 
58
- log "=== Deep Sleep complete for $DATE ==="
73
+ log "=== Deep Sleep v2 complete for $DATE ==="
59
74
  return 0
60
75
  }
61
76
 
@@ -100,36 +100,21 @@ def set_consecutive_failures(count: int):
100
100
 
101
101
 
102
102
  # ── Claude CLI call ──────────────────────────────────────────────────────
103
- CLI_TIMEOUT = 600 # 10 minutes Opus needs time for large prompts
103
+ CLI_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
104
104
 
105
105
 
106
106
  def verify_claude_cli() -> bool:
107
- """Verify Claude CLI is available and authenticated with a real prompt test."""
108
- try:
109
- auth_check = subprocess.run(
110
- [str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
111
- capture_output=True, text=True, timeout=15
112
- )
113
- if auth_check.returncode != 0:
114
- stderr = auth_check.stderr[:200] if auth_check.stderr else ""
115
- log(f"Claude CLI not authenticated or unavailable: {stderr}")
116
- return False
117
- return True
118
- except Exception as e:
119
- log(f"Claude CLI check failed: {e}")
120
- return False
121
-
122
-
123
107
  def call_claude_cli(prompt: str) -> str:
124
108
  """Call claude -p prompt --model opus via subprocess. Returns stdout text."""
125
109
  env = os.environ.copy()
110
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
126
111
  env.pop("CLAUDECODE", None)
127
112
  env.pop("CLAUDE_CODE", None)
128
113
 
129
114
  result = subprocess.run(
130
115
  [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
131
- "--output-format", "text", "--bare",
132
- "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash"],
116
+ "--output-format", "text",
117
+ "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
133
118
  capture_output=True,
134
119
  text=True,
135
120
  timeout=CLI_TIMEOUT,
@@ -424,7 +409,7 @@ def run():
424
409
  return
425
410
 
426
411
  # Call Opus via claude -p
427
- log("Calling claude -p --model opus --bare...")
412
+ log("Calling claude -p --model opus...")
428
413
  try:
429
414
  raw_response = call_claude_cli(prompt)
430
415
  except Exception as e:
@@ -61,7 +61,8 @@ def main():
61
61
  cutoff = (date.today() - timedelta(days=14)).isoformat()
62
62
  stale = conn.execute(
63
63
  "SELECT id, description, date, updated_at FROM followups "
64
- "WHERE status NOT LIKE 'COMPLETED%' AND status NOT LIKE 'COMPLETED%' "
64
+ "WHERE status NOT LIKE 'COMPLETED%' "
65
+ "AND status NOT IN ('DELETED','archived','blocked','waiting') "
65
66
  "AND date != '' AND date < ? "
66
67
  "ORDER BY date",
67
68
  (cutoff,)
@@ -75,7 +76,8 @@ def main():
75
76
  # 3. Orphaned followups (no date, no recent update)
76
77
  orphans = conn.execute(
77
78
  "SELECT id, description FROM followups "
78
- "WHERE status NOT LIKE 'COMPLETED%' AND status NOT LIKE 'COMPLETED%' "
79
+ "WHERE status NOT LIKE 'COMPLETED%' "
80
+ "AND status NOT IN ('DELETED','archived','blocked','waiting') "
79
81
  "AND (date IS NULL OR date = '') "
80
82
  "ORDER BY id"
81
83
  ).fetchall()
@@ -36,7 +36,7 @@ def gh_api(endpoint: str) -> dict | list | None:
36
36
  try:
37
37
  result = subprocess.run(
38
38
  ["gh", "api", endpoint],
39
- capture_output=True, text=True, timeout=30
39
+ capture_output=True, text=True, timeout=21600
40
40
  )
41
41
  if result.returncode == 0:
42
42
  return json.loads(result.stdout)
@@ -109,7 +109,7 @@ def collect_data():
109
109
  try:
110
110
  result = subprocess.run(
111
111
  ["gh", "api", f"repos/{REPO}/compare/{tag}...main"],
112
- capture_output=True, text=True, timeout=30
112
+ capture_output=True, text=True, timeout=21600
113
113
  )
114
114
  if result.returncode == 0:
115
115
  compare = json.loads(result.stdout)
@@ -157,24 +157,21 @@ Return as JSON:
157
157
  "alerts": ["alert1", ...],
158
158
  "release_recommendation": "text or null"
159
159
  }}"""
160
-
161
- auth_check = subprocess.run(
162
- [str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
163
- capture_output=True, text=True, timeout=15
164
160
  )
165
161
  if auth_check.returncode != 0:
166
162
  # CLI not authenticated, skip gracefully
167
163
  return ""
168
164
 
169
165
  env = os.environ.copy()
166
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
170
167
  env.pop("CLAUDECODE", None)
171
168
  env.pop("CLAUDE_CODE", None)
172
169
 
173
170
  result = subprocess.run(
174
171
  [str(CLAUDE_CLI), "-p", prompt,
175
- "--model", "opus", "--output-format", "text", "--bare",
176
- "--allowedTools", "Read,Write,Edit,Glob,Grep"],
177
- capture_output=True, text=True, timeout=180, env=env
172
+ "--model", "opus", "--output-format", "text",
173
+ "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
174
+ capture_output=True, text=True, timeout=21600, env=env
178
175
  )
179
176
 
180
177
  if result.returncode != 0:
@@ -901,30 +901,17 @@ Raw findings:
901
901
  Write the report. Be concise — max 40 lines."""
902
902
 
903
903
  print("\n[TRIAGE] Running CLI interpretation...")
904
-
905
- # Verify Claude CLI is authenticated before calling
906
- try:
907
- auth_check = subprocess.run(
908
- [str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
909
- capture_output=True, text=True, timeout=15
910
- )
911
- if auth_check.returncode != 0:
912
- print("[TRIAGE] Claude CLI not available or not authenticated. Skipping triage.")
913
- return
914
- except Exception:
915
- print("[TRIAGE] Claude CLI check failed. Skipping triage.")
916
- return
917
-
918
904
  env = os.environ.copy()
905
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
919
906
  env.pop("CLAUDECODE", None)
920
907
  env.pop("CLAUDE_CODE", None)
921
908
 
922
909
  try:
923
910
  result = subprocess.run(
924
911
  [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
925
- "--output-format", "text", "--bare",
926
- "--allowedTools", "Read,Write,Edit,Glob,Grep"],
927
- capture_output=True, text=True, timeout=120, env=env
912
+ "--output-format", "text",
913
+ "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
914
+ capture_output=True, text=True, timeout=21600, env=env
928
915
  )
929
916
  if result.returncode == 0:
930
917
  print(f"[TRIAGE] Report written to {triage_file}")
@@ -111,35 +111,6 @@ Rules:
111
111
 
112
112
  # Try CLI first, fall back to mechanical similarity
113
113
  if CLAUDE_CLI.exists():
114
- auth_check = subprocess.run(
115
- [str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
116
- capture_output=True, text=True, timeout=15
117
- )
118
- if auth_check.returncode != 0:
119
- # CLI not authenticated, skip gracefully
120
- return {"known": False, "confidence": 0, "recommendation": "CLI not authenticated — skipped validation", "matching_learnings": []}
121
-
122
- env = os.environ.copy()
123
- env.pop("CLAUDECODE", None)
124
- env.pop("CLAUDE_CODE", None)
125
-
126
- try:
127
- result = subprocess.run(
128
- [str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text", "--bare",
129
- "--allowedTools", "Read,Write,Edit,Glob,Grep"],
130
- capture_output=True, text=True, timeout=60, env=env
131
- )
132
- if result.returncode == 0:
133
- text = result.stdout.strip()
134
- # Strip markdown fences if present
135
- if "```json" in text:
136
- text = text.split("```json")[1].split("```")[0]
137
- elif "```" in text:
138
- text = text.split("```")[1].split("```")[0]
139
- return json.loads(text.strip())
140
- except (subprocess.TimeoutExpired, json.JSONDecodeError, Exception):
141
- pass # Fall through to mechanical fallback
142
-
143
114
  # Fallback: mechanical SequenceMatcher (original logic)
144
115
  return _mechanical_validate(finding, learnings)
145
116
 
@@ -113,7 +113,7 @@ def consolidate_with_cli(data: dict) -> bool:
113
113
 
114
114
  diaries_with_critique = [
115
115
  d for d in data["diaries"]
116
- if d.get("self_critique") and not (d["self_critique"] or "").strip().lower().startswith("no self-critique")
116
+ if d.get("self_critique") and not str(d["self_critique"]).strip().lower().startswith("no self-critique")
117
117
  ]
118
118
 
119
119
  if not diaries_with_critique:
@@ -125,8 +125,10 @@ def consolidate_with_cli(data: dict) -> bool:
125
125
  if len(diaries_json) > 12000:
126
126
  diaries_json = diaries_json[:12000] + "\n... (truncated)"
127
127
 
128
- prompt = f"""You are NEXO's nightly consolidator. Your job is to review the self-critiques
129
- from today and decide which deserve to become permanent rules (feedback_postmortem_*.md).
128
+ prompt = f"""FIRST: Call nexo_startup(task='nightly postmortem consolidation') to register this session.
129
+
130
+ You are NEXO's nightly consolidator. Your job is to review the self-critiques
131
+ from today and decide which deserve to become permanent rules. Use nexo_learning_add for permanent rules and nexo_followup_create for action items.
130
132
 
131
133
  DATE: {data['date']}
132
134
  SESSIONS TODAY: {len(data['diaries'])} total, {len(diaries_with_critique)} with self-critique
@@ -185,30 +187,17 @@ INSTRUCTIONS:
185
187
  Execute without asking."""
186
188
 
187
189
  log(f"Stage 2: Invoking Claude CLI (opus) with {len(diaries_with_critique)} critiques...")
188
-
189
- # Verify Claude CLI is authenticated before calling
190
- try:
191
- auth_check = subprocess.run(
192
- [str(CLAUDE_CLI), "-p", "Reply with exactly: ok", "--bare", "--output-format", "text", "--model", "haiku"],
193
- capture_output=True, text=True, timeout=15
194
- )
195
- if auth_check.returncode != 0:
196
- log("Stage 2: Claude CLI not available or not authenticated. Skipping Stage 2.")
197
- return False
198
- except Exception:
199
- log("Stage 2: Claude CLI check failed. Skipping Stage 2.")
200
- return False
201
-
202
190
  env = os.environ.copy()
191
+ env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
203
192
  env.pop("CLAUDECODE", None)
204
193
  env.pop("CLAUDE_CODE", None)
205
194
 
206
195
  try:
207
196
  result = subprocess.run(
208
197
  [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
209
- "--output-format", "text", "--bare",
210
- "--allowedTools", "Read,Write,Edit,Glob,Grep"],
211
- capture_output=True, text=True, timeout=300, env=env
198
+ "--output-format", "text",
199
+ "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
200
+ capture_output=True, text=True, timeout=21600, env=env
212
201
  )
213
202
 
214
203
  if result.returncode != 0:
@@ -38,6 +38,7 @@ def check_overdue_followups() -> list[dict]:
38
38
  SELECT id, description, date, created_at, reasoning
39
39
  FROM followups
40
40
  WHERE status NOT LIKE 'COMPLETED%'
41
+ AND status NOT IN ('DELETED','archived','blocked','waiting')
41
42
  AND date IS NOT NULL AND date != ''
42
43
  ORDER BY date ASC
43
44
  """).fetchall()