nexo-brain 5.3.19 → 5.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/nexo-brain.js +52 -10
  3. package/package.json +1 -1
  4. package/src/auto_update.py +11 -8
  5. package/src/dashboard/static/favicon 2.svg +32 -0
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +40 -0
  8. package/src/dashboard/static/style 2.css +2458 -0
  9. package/src/dashboard/templates/adaptive 2.html +118 -0
  10. package/src/dashboard/templates/artifacts 2.html +133 -0
  11. package/src/dashboard/templates/backups 2.html +136 -0
  12. package/src/dashboard/templates/base 2.html +417 -0
  13. package/src/dashboard/templates/calendar 2.html +591 -0
  14. package/src/dashboard/templates/chat 2.html +356 -0
  15. package/src/dashboard/templates/claims 2.html +259 -0
  16. package/src/dashboard/templates/cortex 2.html +321 -0
  17. package/src/dashboard/templates/credentials 2.html +128 -0
  18. package/src/dashboard/templates/crons 2.html +370 -0
  19. package/src/dashboard/templates/dashboard 2.html +494 -0
  20. package/src/dashboard/templates/dreams 2.html +252 -0
  21. package/src/dashboard/templates/email 2.html +160 -0
  22. package/src/dashboard/templates/evolution 2.html +189 -0
  23. package/src/dashboard/templates/feed 2.html +249 -0
  24. package/src/dashboard/templates/followup_health 2.html +170 -0
  25. package/src/dashboard/templates/graph 2.html +201 -0
  26. package/src/dashboard/templates/guard 2.html +259 -0
  27. package/src/dashboard/templates/inbox 2.html +251 -0
  28. package/src/dashboard/templates/memory 2.html +420 -0
  29. package/src/dashboard/templates/operations 2.html +608 -0
  30. package/src/dashboard/templates/plugins 2.html +185 -0
  31. package/src/dashboard/templates/protocol 2.html +199 -0
  32. package/src/dashboard/templates/rules 2.html +246 -0
  33. package/src/dashboard/templates/sentiment 2.html +247 -0
  34. package/src/dashboard/templates/sessions 2.html +218 -0
  35. package/src/dashboard/templates/skills 2.html +329 -0
  36. package/src/dashboard/templates/somatic 2.html +73 -0
  37. package/src/dashboard/templates/triggers 2.html +133 -0
  38. package/src/dashboard/templates/trust 2.html +360 -0
  39. package/src/db/__init__ 2.py +259 -0
  40. package/src/db/_core 2.py +437 -0
  41. package/src/db/_credentials 2.py +124 -0
  42. package/src/db/_episodic 2.py +762 -0
  43. package/src/db/_evolution 2.py +54 -0
  44. package/src/db/_fts 2.py +406 -0
  45. package/src/db/_goal_profiles 2.py +376 -0
  46. package/src/db/_hot_context 2.py +660 -0
  47. package/src/db/_outcomes 2.py +800 -0
  48. package/src/db/_personal_scripts 2.py +582 -0
  49. package/src/db/_sessions 2.py +330 -0
  50. package/src/db/_tasks 2.py +91 -0
  51. package/src/db/_watchers 2.py +173 -0
  52. package/src/doctor/formatters 2.py +52 -0
  53. package/src/doctor/models 2.py +69 -0
  54. package/src/doctor/planes 2.py +87 -0
  55. package/src/doctor/providers/__init__ 2.py +1 -0
  56. package/src/doctor/providers/deep 2.py +367 -0
  57. package/src/evolution_cycle 2.py +519 -0
  58. package/src/hooks/auto_capture 2.py +208 -0
  59. package/src/hooks/caffeinate-guard 2.sh +8 -0
  60. package/src/hooks/capture-session 2.sh +21 -0
  61. package/src/hooks/capture-tool-logs 2.sh +158 -0
  62. package/src/hooks/daily-briefing-check 2.sh +33 -0
  63. package/src/hooks/heartbeat-enforcement 2.py +90 -0
  64. package/src/hooks/heartbeat-posttool 2.sh +18 -0
  65. package/src/hooks/inbox-hook 2.sh +76 -0
  66. package/src/hooks/post-compact 2.sh +152 -0
  67. package/src/hooks/pre-compact 2.sh +169 -0
  68. package/src/hooks/protocol-guardrail 2.sh +10 -0
  69. package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
  70. package/src/hooks/session-stop 2.sh +52 -0
  71. package/src/kg_populate 2.py +292 -0
  72. package/src/maintenance 2.py +53 -0
  73. package/src/memory_backends 2.py +71 -0
  74. package/src/migrate_embeddings 2.py +124 -0
  75. package/src/nexo_sdk 2.py +103 -0
  76. package/src/observability 2.py +199 -0
  77. package/src/plugin_loader 2.py +217 -0
  78. package/src/plugins/__init__ 2.py +0 -0
  79. package/src/plugins/artifact_registry 2.py +450 -0
  80. package/src/plugins/backup 2.py +127 -0
  81. package/src/plugins/claims_tools 2.py +119 -0
  82. package/src/plugins/cognitive_memory 2.py +609 -0
  83. package/src/plugins/core_rules 2.py +252 -0
  84. package/src/plugins/cortex 2.py +1155 -0
  85. package/src/plugins/entities 2.py +67 -0
  86. package/src/plugins/episodic_memory 2.py +560 -0
  87. package/src/plugins/evolution 2.py +167 -0
  88. package/src/plugins/goal_engine 2.py +142 -0
  89. package/src/plugins/guard 2.py +862 -0
  90. package/src/plugins/impact 2.py +29 -0
  91. package/src/plugins/knowledge_graph_tools 2.py +137 -0
  92. package/src/plugins/media_memory_tools 2.py +98 -0
  93. package/src/plugins/memory_export 2.py +196 -0
  94. package/src/plugins/outcomes 2.py +130 -0
  95. package/src/plugins/personal_scripts 2.py +117 -0
  96. package/src/plugins/preferences 2.py +47 -0
  97. package/src/plugins/protocol 2.py +1449 -0
  98. package/src/plugins/simple_api 2.py +106 -0
  99. package/src/plugins/skills 2.py +341 -0
  100. package/src/plugins/state_watchers 2.py +79 -0
  101. package/src/plugins/update 2.py +986 -0
  102. package/src/plugins/user_state_tools 2.py +43 -0
  103. package/src/plugins/workflow 2.py +588 -0
  104. package/src/protocol_settings 2.py +59 -0
  105. package/src/public_contribution 2.py +466 -0
  106. package/src/public_evolution_queue 2.py +241 -0
  107. package/src/requirements 2.txt +14 -0
  108. package/src/retroactive_learnings 2.py +373 -0
  109. package/src/rules/__init__ 2.py +0 -0
  110. package/src/rules/core-rules 2.json +331 -0
  111. package/src/rules/migrate 2.py +207 -0
  112. package/src/runtime_power 2.py +874 -0
  113. package/src/script_registry 2.py +1559 -0
  114. package/src/scripts/check-context 2.py +272 -0
  115. package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
  116. package/src/scripts/deep-sleep/collect 2.py +928 -0
  117. package/src/scripts/deep-sleep/extract 2.py +330 -0
  118. package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
  119. package/src/scripts/deep-sleep/synthesize 2.py +312 -0
  120. package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
  121. package/src/scripts/nexo-agent-run 2.py +75 -0
  122. package/src/scripts/nexo-auto-update 2.py +6 -0
  123. package/src/scripts/nexo-backup 2.sh +25 -0
  124. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  125. package/src/scripts/nexo-catchup 2.py +300 -0
  126. package/src/scripts/nexo-cognitive-decay 2.py +257 -0
  127. package/src/scripts/nexo-cortex-cycle 2.py +293 -0
  128. package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
  129. package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
  130. package/src/scripts/nexo-dashboard 2.sh +29 -0
  131. package/src/scripts/nexo-deep-sleep 2.sh +86 -0
  132. package/src/scripts/nexo-evolution-run 2.py +1664 -0
  133. package/src/scripts/nexo-followup-hygiene 2.py +139 -0
  134. package/src/scripts/nexo-hook-record 2.py +42 -0
  135. package/src/scripts/nexo-immune 2.py +936 -0
  136. package/src/scripts/nexo-impact-scorer 2.py +117 -0
  137. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  138. package/src/scripts/nexo-install 2.py +6 -0
  139. package/src/scripts/nexo-learning-housekeep 2.py +401 -0
  140. package/src/scripts/nexo-learning-validator 2.py +266 -0
  141. package/src/scripts/nexo-migrate 2.py +260 -0
  142. package/src/scripts/nexo-outcome-checker 2.py +127 -0
  143. package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
  144. package/src/scripts/nexo-pre-commit 2.py +120 -0
  145. package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
  146. package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
  147. package/src/scripts/nexo-reflection 2.py +256 -0
  148. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  149. package/src/scripts/nexo-sleep 2.py +631 -0
  150. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  151. package/src/scripts/nexo-sync-clients 2.py +16 -0
  152. package/src/scripts/nexo-synthesis 2.py +475 -0
  153. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  154. package/src/scripts/nexo-update 2.sh +306 -0
  155. package/src/scripts/nexo-watchdog 2.sh +1207 -0
  156. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  157. package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
  158. package/src/server 2.py +1296 -0
  159. package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
  160. package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
  161. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
  162. package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
  163. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
  164. package/src/skills/run-release-final-audit/guide 2.md +16 -0
  165. package/src/skills/run-release-final-audit/script 2.py +259 -0
  166. package/src/skills/run-release-final-audit/skill 2.json +77 -0
  167. package/src/skills/run-runtime-doctor/guide 2.md +12 -0
  168. package/src/skills/run-runtime-doctor/script 2.py +21 -0
  169. package/src/skills/run-runtime-doctor/skill 2.json +25 -0
  170. package/src/skills_runtime 2.py +932 -0
  171. package/src/state_watchers_runtime 2.py +475 -0
  172. package/src/storage_router 2.py +32 -0
  173. package/src/system_catalog 2.py +786 -0
  174. package/src/tools_coordination 2.py +103 -0
  175. package/src/tools_credentials 2.py +68 -0
  176. package/src/tools_drive 2.py +487 -0
  177. package/src/tools_hot_context 2.py +163 -0
  178. package/src/tools_learnings 2.py +612 -0
  179. package/src/tools_menu 2.py +229 -0
  180. package/src/tools_reminders 2.py +88 -0
  181. package/src/tools_reminders_crud 2.py +363 -0
  182. package/src/tools_sessions 2.py +1054 -0
  183. package/src/tools_system_catalog 2.py +19 -0
  184. package/src/tools_task_history 2.py +57 -0
  185. package/src/tools_transcripts 2.py +98 -0
  186. package/src/transcript_utils 2.py +412 -0
  187. package/src/user_context 2.py +46 -0
  188. package/src/user_data_portability 2.py +328 -0
  189. package/src/user_state_model 2.py +170 -0
  190. package/templates/CLAUDE.md 2.template +108 -0
  191. package/templates/CODEX.AGENTS.md 2.template +66 -0
  192. package/templates/launchagents/README 2.md +132 -0
  193. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
  194. package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
  195. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
  196. package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
  197. package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
  198. package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
  199. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
  200. package/templates/launchagents/com.nexo.immune 2.plist +41 -0
  201. package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
  202. package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
  203. package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
  204. package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
  205. package/templates/nexo_helper 2.py +301 -0
  206. package/templates/openclaw 2.json +13 -0
  207. package/templates/plugin-template 2.py +40 -0
  208. package/templates/script-template 2.py +59 -0
  209. package/templates/script-template 2.sh +13 -0
  210. package/templates/skill-script-template 2.py +48 -0
  211. package/templates/skill-template 2.md +33 -0
@@ -0,0 +1,475 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ NEXO Synthesis Engine v2 — Daily intelligence brief.
4
+
5
+ Before: ~400 lines of Python concatenating SQL results into markdown sections.
6
+ Now: Collects raw data, passes to the configured automation backend which synthesizes
7
+ with real understanding of what matters for tomorrow.
8
+
9
+ Runs daily at 06:00 via LaunchAgent.
10
+ """
11
+
12
+ import fcntl
13
+ import json
14
+ import os
15
+ import sqlite3
16
+ import subprocess
17
+ import sys
18
+ from datetime import datetime, date, timedelta
19
+ from pathlib import Path
20
+
21
+
22
+ try:
23
+ from client_preferences import resolve_user_model as _resolve_user_model
24
+ _USER_MODEL = _resolve_user_model()
25
+ except Exception:
26
+ _USER_MODEL = ""
27
+
28
+ HOME = Path.home()
29
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
30
+ _script_dir = Path(__file__).resolve().parent
31
+ _repo_src = _script_dir.parent
32
+ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
33
+ if str(NEXO_CODE) not in sys.path:
34
+ sys.path.insert(0, str(NEXO_CODE))
35
+
36
+ from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
37
+
38
+ CLAUDE_DIR = NEXO_HOME
39
+ COORD_DIR = CLAUDE_DIR / "coordination"
40
+ NEXO_DB = NEXO_HOME / "data" / "nexo.db"
41
+ OUTPUT_FILE = COORD_DIR / "daily-synthesis.md"
42
+ LAST_RUN_FILE = COORD_DIR / "synthesis-last-run"
43
+ LOCK_FILE = COORD_DIR / "synthesis.lock"
44
+ def _resolve_claude_cli() -> Path:
45
+ """Find claude CLI: saved path > PATH > common locations."""
46
+ import shutil as _shutil
47
+ saved = NEXO_HOME / "config" / "claude-cli-path"
48
+ if saved.exists():
49
+ p = Path(saved.read_text().strip())
50
+ if p.exists():
51
+ return p
52
+ found = _shutil.which("claude")
53
+ if found:
54
+ return Path(found)
55
+ for candidate in [
56
+ HOME / ".local" / "bin" / "claude",
57
+ HOME / ".npm-global" / "bin" / "claude",
58
+ Path("/usr/local/bin/claude"),
59
+ ]:
60
+ if candidate.exists():
61
+ return candidate
62
+ return HOME / ".local" / "bin" / "claude"
63
+
64
+ CLAUDE_CLI = _resolve_claude_cli()
65
+
66
+ TODAY = date.today()
67
+ TODAY_STR = TODAY.isoformat()
68
+
69
+
70
+ def log(msg: str):
71
+ ts = datetime.now().strftime("%H:%M:%S")
72
+ print(f"[{ts}] {msg}", flush=True)
73
+
74
+
75
+ def should_run() -> bool:
76
+ if LAST_RUN_FILE.exists():
77
+ return LAST_RUN_FILE.read_text().strip() != TODAY_STR
78
+ return True
79
+
80
+
81
+ def mark_done():
82
+ LAST_RUN_FILE.write_text(TODAY_STR)
83
+
84
+
85
+ def acquire_lock():
86
+ lock_fd = open(LOCK_FILE, "w")
87
+ try:
88
+ fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
89
+ return lock_fd
90
+ except BlockingIOError:
91
+ log("Another instance running. Exiting.")
92
+ sys.exit(0)
93
+
94
+
95
+ def release_lock(lock_fd):
96
+ fcntl.flock(lock_fd, fcntl.LOCK_UN)
97
+ lock_fd.close()
98
+ LOCK_FILE.unlink(missing_ok=True)
99
+
100
+
101
+ def safe_query(sql: str, params=()) -> list:
102
+ if not NEXO_DB.exists():
103
+ return []
104
+ try:
105
+ conn = sqlite3.connect(str(NEXO_DB))
106
+ conn.row_factory = sqlite3.Row
107
+ rows = [dict(r) for r in conn.execute(sql, params).fetchall()]
108
+ conn.close()
109
+ return rows
110
+ except Exception as e:
111
+ log(f"Query error: {e}")
112
+ return []
113
+
114
+
115
+ def _table_columns(table_name: str) -> set[str]:
116
+ if not NEXO_DB.exists():
117
+ return set()
118
+ try:
119
+ conn = sqlite3.connect(str(NEXO_DB))
120
+ rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall()
121
+ conn.close()
122
+ return {str(row[1]) for row in rows}
123
+ except Exception:
124
+ return set()
125
+
126
+
127
+ def _parse_json_field(value):
128
+ if isinstance(value, str):
129
+ stripped = value.strip()
130
+ if not stripped:
131
+ return {}
132
+ try:
133
+ parsed = json.loads(stripped)
134
+ except json.JSONDecodeError:
135
+ return {}
136
+ return parsed if isinstance(parsed, dict) else {}
137
+ return value if isinstance(value, dict) else {}
138
+
139
+
140
+ def _impact_reasoning(row: dict) -> str:
141
+ factors = _parse_json_field(row.get("impact_factors"))
142
+ return str(factors.get("reasoning") or "").strip()
143
+
144
+
145
+ def _load_json_summary(path: Path, *, actionable) -> tuple[dict | None, str | None]:
146
+ if not path.exists():
147
+ return None, None
148
+ try:
149
+ payload = json.loads(path.read_text(encoding="utf-8"))
150
+ except Exception as exc:
151
+ return None, str(exc)
152
+ if not isinstance(payload, dict):
153
+ return None, "summary payload is not a JSON object"
154
+ if not actionable(payload):
155
+ return None, None
156
+ return payload, None
157
+
158
+
159
+ def _load_coordination_summary(filename: str, *, actionable) -> tuple[dict | None, str | None]:
160
+ return _load_json_summary(COORD_DIR / filename, actionable=actionable)
161
+
162
+
163
+ def _update_summary_actionable(payload: dict) -> bool:
164
+ if any(payload.get(key) for key in ("error", "updated", "deferred_reason", "git_update", "npm_notice")):
165
+ return True
166
+ for action in payload.get("actions") or []:
167
+ if str(action).startswith("personal-schedules-"):
168
+ return True
169
+ for message in payload.get("client_bootstrap_updates") or []:
170
+ if "already current" not in str(message).lower():
171
+ return True
172
+ return False
173
+
174
+
175
+ def collect_data() -> dict:
176
+ """Collect all raw data for synthesis."""
177
+ data = {"date": TODAY_STR}
178
+
179
+ # Today's learnings
180
+ data["learnings"] = safe_query(
181
+ "SELECT category, title, content, reasoning FROM learnings "
182
+ "WHERE date(created_at, 'unixepoch') = ? ORDER BY created_at DESC",
183
+ (TODAY_STR,)
184
+ )
185
+
186
+ # Today's decisions
187
+ data["decisions"] = safe_query(
188
+ "SELECT domain, decision, alternatives, based_on, outcome FROM decisions "
189
+ "WHERE date(created_at) = ? ORDER BY created_at DESC",
190
+ (TODAY_STR,)
191
+ )
192
+
193
+ # Today's changes
194
+ data["changes"] = safe_query(
195
+ "SELECT files, what_changed, why, affects, risks FROM change_log "
196
+ "WHERE date(created_at) = ? ORDER BY created_at DESC",
197
+ (TODAY_STR,)
198
+ )
199
+
200
+ # Session diaries (summaries + mental_state)
201
+ data["diaries"] = safe_query(
202
+ "SELECT summary, self_critique, mental_state, user_signals FROM session_diary "
203
+ "WHERE date(created_at) = ? ORDER BY created_at DESC",
204
+ (TODAY_STR,)
205
+ )
206
+
207
+ # Overdue reminders (schema: description, date, status uppercase)
208
+ data["overdue_reminders"] = safe_query(
209
+ "SELECT id, description, date FROM reminders "
210
+ "WHERE status='PENDING' AND date <= ? ORDER BY date",
211
+ (TODAY_STR,)
212
+ )
213
+
214
+ # Pending followups (schema: description, date, status uppercase)
215
+ followup_columns = _table_columns("followups")
216
+ if "impact_score" in followup_columns:
217
+ impact_factors_sql = ", impact_factors" if "impact_factors" in followup_columns else ""
218
+ followup_select = (
219
+ "SELECT id, description, date, priority, impact_score"
220
+ f"{impact_factors_sql} FROM followups "
221
+ )
222
+ followup_order = (
223
+ "ORDER BY "
224
+ "CASE WHEN COALESCE(impact_score, 0) > 0 THEN 0 ELSE 1 END ASC, "
225
+ "COALESCE(impact_score, 0) DESC, "
226
+ "CASE WHEN date IS NULL OR date = '' THEN 1 ELSE 0 END ASC, "
227
+ "date ASC"
228
+ )
229
+ else:
230
+ followup_select = "SELECT id, description, date FROM followups "
231
+ followup_order = "ORDER BY date"
232
+ data["pending_followups"] = safe_query(
233
+ f"{followup_select} WHERE status='PENDING' {followup_order}"
234
+ )
235
+ for row in data["pending_followups"]:
236
+ if "impact_factors" in row:
237
+ row["impact_factors"] = _parse_json_field(row.get("impact_factors"))
238
+ row["impact_reasoning"] = _impact_reasoning(row)
239
+
240
+ impact_summary_file = COORD_DIR / "impact-scorer-summary.json"
241
+ if impact_summary_file.exists():
242
+ try:
243
+ data["impact_queue_summary"] = json.loads(impact_summary_file.read_text(encoding="utf-8"))
244
+ except Exception as exc:
245
+ data["impact_queue_summary_error"] = str(exc)
246
+
247
+ followup_hygiene_summary, followup_hygiene_error = _load_coordination_summary(
248
+ "followup-hygiene-summary.json",
249
+ actionable=lambda payload: any(
250
+ int(payload.get(key, 0) or 0) > 0
251
+ for key in ("dirty_normalized", "stale_count", "orphan_count")
252
+ ),
253
+ )
254
+ if followup_hygiene_summary is not None:
255
+ data["followup_hygiene_summary"] = followup_hygiene_summary
256
+ elif followup_hygiene_error:
257
+ data["followup_hygiene_summary_error"] = followup_hygiene_error
258
+
259
+ outcome_checker_summary, outcome_checker_error = _load_coordination_summary(
260
+ "outcome-checker-summary.json",
261
+ actionable=lambda payload: (
262
+ any(
263
+ int(payload.get(key, 0) or 0) > 0
264
+ for key in ("checked", "met", "missed", "pending", "errors")
265
+ )
266
+ or bool(payload.get("ids"))
267
+ or bool(((payload.get("auto_promoted_patterns") or {}).get("promoted") or []))
268
+ ),
269
+ )
270
+ if outcome_checker_summary is not None:
271
+ data["outcome_checker_summary"] = outcome_checker_summary
272
+ elif outcome_checker_error:
273
+ data["outcome_checker_summary_error"] = outcome_checker_error
274
+
275
+ update_summary, update_summary_error = _load_json_summary(
276
+ NEXO_HOME / "logs" / "update-last-summary.json",
277
+ actionable=_update_summary_actionable,
278
+ )
279
+ if update_summary is not None:
280
+ data["update_summary"] = update_summary
281
+ elif update_summary_error:
282
+ data["update_summary_error"] = update_summary_error
283
+
284
+ # Guard stats
285
+ data["guard_stats"] = safe_query(
286
+ "SELECT category, COUNT(*) as cnt FROM learnings WHERE status='active' "
287
+ "GROUP BY category ORDER BY cnt DESC LIMIT 10"
288
+ )
289
+
290
+ # Postmortem daily (if exists)
291
+ pm_file = COORD_DIR / "postmortem-daily.md"
292
+ if pm_file.exists():
293
+ data["postmortem_summary"] = pm_file.read_text()[:2000]
294
+
295
+ return data
296
+
297
+
298
+ def synthesize(data: dict) -> bool:
299
+ """CLI synthesizes the daily brief."""
300
+
301
+ data_json = json.dumps(data, ensure_ascii=False, indent=1)
302
+ if len(data_json) > 15000:
303
+ data_json = data_json[:15000] + "\n... (truncated)"
304
+
305
+ prompt = f"""FIRST: Call nexo_startup(task='daily synthesis') to register this session.
306
+
307
+ You are NEXO's synthesis engine. Write the daily intelligence brief for tomorrow's
308
+ startup. This file is read by NEXO at the beginning of each session to understand
309
+ what happened today and what to focus on tomorrow. Use nexo_learning_add and nexo_followup_create if you discover actionable items.
310
+
311
+ TODAY'S RAW DATA:
312
+ {data_json}
313
+
314
+ Write the synthesis to {OUTPUT_FILE} with this structure:
315
+
316
+ # NEXO Daily Synthesis — {TODAY_STR}
317
+
318
+ ## Errors & Learnings
319
+ [New learnings from today — what went wrong, what was learned]
320
+
321
+ ## Decisions Made
322
+ [Key decisions and their reasoning]
323
+
324
+ ## Changes Deployed
325
+ [What was changed in production today]
326
+
327
+ ## the user — Observations
328
+ [Patterns in the user's behavior: frustrations, pending decisions, ideas without
329
+ deadlines, topics he started but didn't close. This is NEXO's peripheral vision.]
330
+
331
+ ## Weak Points (self-assessment)
332
+ [Where NEXO failed or could have done better today — from session diaries]
333
+
334
+ ## Tomorrow's Context
335
+ [What the next session needs to know: pending followups, overdue reminders,
336
+ in-progress tasks, things to verify]
337
+
338
+ ## Guard Status
339
+ [Areas with most learnings — where errors concentrate]
340
+
341
+ Be concise. Each section 3-8 bullet points max. Focus on what CHANGES BEHAVIOR,
342
+ not what merely happened. If a section has nothing, write "Nothing notable."
343
+
344
+ Execute without asking."""
345
+
346
+ log("Invoking automation backend for synthesis...")
347
+ try:
348
+ result = run_automation_prompt(
349
+ prompt,
350
+ model=_USER_MODEL or "opus",
351
+ timeout=21600,
352
+ output_format="text",
353
+ allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
354
+ )
355
+
356
+ if result.returncode != 0:
357
+ log(f"CLI error ({result.returncode}): {(result.stderr or '')[:300]}")
358
+ return False
359
+
360
+ log(f"Synthesis complete. Output: {len(result.stdout or '')} chars")
361
+ return True
362
+
363
+ except AutomationBackendUnavailableError as e:
364
+ log(f"Automation backend unavailable: {e}")
365
+ return False
366
+ except subprocess.TimeoutExpired:
367
+ log("CLI timed out (180s)")
368
+ return False
369
+ except Exception as e:
370
+ log(f"Exception: {e}")
371
+ return False
372
+
373
+
374
+ def fallback_synthesis(data: dict):
375
+ """Write a basic synthesis from raw data when CLI is unavailable."""
376
+ log("Fallback: writing basic synthesis from raw data...")
377
+ lines = [f"# NEXO Daily Synthesis -- {TODAY_STR}", "",
378
+ "*(Generated by fallback -- CLI was unavailable)*", ""]
379
+
380
+ if data.get("learnings"):
381
+ lines.append("## Errors & Learnings")
382
+ for l in data["learnings"][:10]:
383
+ lines.append(f"- [{l.get('category', 'general')}] {l.get('title', 'untitled')}")
384
+ lines.append("")
385
+
386
+ if data.get("decisions"):
387
+ lines.append("## Decisions Made")
388
+ for d in data["decisions"][:10]:
389
+ lines.append(f"- [{d.get('domain', 'general')}] {d.get('decision', '')[:120]}")
390
+ lines.append("")
391
+
392
+ if data.get("changes"):
393
+ lines.append("## Changes Deployed")
394
+ for c in data["changes"][:10]:
395
+ lines.append(f"- {c.get('what_changed', '')[:120]}")
396
+ lines.append("")
397
+
398
+ if data.get("overdue_reminders"):
399
+ lines.append("## Overdue Reminders")
400
+ for r in data["overdue_reminders"][:10]:
401
+ lines.append(f"- #{r.get('id', '?')} {r.get('description', '')} (due {r.get('date', '?')})")
402
+ lines.append("")
403
+
404
+ if data.get("pending_followups"):
405
+ lines.append("## Pending Followups")
406
+ for f in data["pending_followups"][:10]:
407
+ impact = float(f.get("impact_score") or 0.0)
408
+ impact_tag = f" [impact {impact:.1f}]" if impact > 0 else ""
409
+ because = _impact_reasoning(f)
410
+ because_tag = f" — {because}" if because else ""
411
+ lines.append(
412
+ f"- #{f.get('id', '?')} {f.get('description', '')} "
413
+ f"(due {f.get('date', '?')}){impact_tag}{because_tag}"
414
+ )
415
+ lines.append("")
416
+
417
+ impact_summary = data.get("impact_queue_summary") or {}
418
+ if impact_summary.get("top_changes"):
419
+ lines.append("## Queue Changes By Impact")
420
+ for item in impact_summary.get("top_changes", [])[:5]:
421
+ delta = float(item.get("delta") or 0.0)
422
+ if abs(delta) < 1.0:
423
+ continue
424
+ direction = "+" if delta >= 0 else ""
425
+ lines.append(
426
+ f"- #{item.get('id', '?')} {direction}{delta:.1f} -> {float(item.get('impact_score') or 0.0):.1f}"
427
+ f" ({item.get('impact_reasoning') or 'score recalculated'})"
428
+ )
429
+ lines.append("")
430
+
431
+ OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
432
+ OUTPUT_FILE.write_text("\n".join(lines))
433
+ log(f"Fallback synthesis written to {OUTPUT_FILE}")
434
+
435
+
436
+ def main():
437
+ if not should_run():
438
+ log(f"Already ran today ({TODAY_STR}). Skipping.")
439
+ return
440
+
441
+ lock_fd = acquire_lock()
442
+ try:
443
+ log(f"=== NEXO Synthesis v2 -- {TODAY_STR} ===")
444
+
445
+ data = collect_data()
446
+ log(f"Collected: {len(data.get('learnings', []))} learnings, "
447
+ f"{len(data.get('decisions', []))} decisions, "
448
+ f"{len(data.get('changes', []))} changes, "
449
+ f"{len(data.get('diaries', []))} diaries")
450
+
451
+ success = synthesize(data)
452
+
453
+ if success:
454
+ mark_done()
455
+ log("Synthesis v2 complete.")
456
+ else:
457
+ log("Synthesis CLI failed -- writing fallback synthesis.")
458
+ fallback_synthesis(data)
459
+ mark_done()
460
+
461
+ # Register for catch-up
462
+ try:
463
+ state_file = NEXO_HOME / "operations" / ".catchup-state.json"
464
+ st = json.loads(state_file.read_text()) if state_file.exists() else {}
465
+ st["synthesis"] = datetime.now().isoformat()
466
+ state_file.write_text(json.dumps(st, indent=2))
467
+ except Exception:
468
+ pass
469
+
470
+ finally:
471
+ release_lock(lock_fd)
472
+
473
+
474
+ if __name__ == "__main__":
475
+ main()
@@ -0,0 +1,79 @@
1
+ #!/bin/bash
2
+ # NEXO TCC Auto-Approve — grants macOS permissions to new Claude Code versions.
3
+ #
4
+ # macOS only. On Linux this is a no-op (Linux doesn't have TCC).
5
+ # Runs at load to approve any new Claude versions that appeared.
6
+ #
7
+ # What it does:
8
+ # 1. Scans ~/.local/share/claude/versions/ for Claude binaries
9
+ # 2. For each new version, grants TCC access to Documents, Desktop, Downloads, etc.
10
+ # 3. Also approves the Python binary used by NEXO's venv
11
+ # 4. Tracks which versions have been approved to avoid re-processing
12
+ #
13
+ # Why: Claude Code updates frequently. Each new binary needs macOS permission
14
+ # grants or the user gets popup dialogs interrupting their work.
15
+
16
+ set -euo pipefail
17
+
18
+ # Linux: nothing to do
19
+ if [ "$(uname -s)" != "Darwin" ]; then
20
+ exit 0
21
+ fi
22
+
23
+ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
24
+ TCC_DB="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
25
+ VERSIONS_DIR="$HOME/.local/share/claude/versions"
26
+ MARKER_DIR="$NEXO_HOME/data/.tcc-approved"
27
+ LOG="$NEXO_HOME/logs/tcc-auto-approve.log"
28
+
29
+ mkdir -p "$MARKER_DIR" "$(dirname "$LOG")"
30
+
31
+ # TCC services Claude Code needs
32
+ SERVICES=(
33
+ kTCCServiceSystemPolicyDocumentsFolder
34
+ kTCCServiceSystemPolicyDesktopFolder
35
+ kTCCServiceSystemPolicyDownloadsFolder
36
+ kTCCServiceMediaLibrary
37
+ kTCCServiceSystemPolicyNetworkVolumes
38
+ kTCCServiceSystemPolicyAppData
39
+ kTCCServiceFileProviderDomain
40
+ )
41
+
42
+ # Approve Claude versions
43
+ if [ -d "$VERSIONS_DIR" ]; then
44
+ for bin_path in "$VERSIONS_DIR"/*; do
45
+ [ ! -e "$bin_path" ] && continue
46
+ version=$(basename "$bin_path")
47
+ marker="$MARKER_DIR/$version"
48
+
49
+ # Skip if already approved
50
+ [ -f "$marker" ] && continue
51
+
52
+ echo "$(date '+%Y-%m-%d %H:%M:%S') Approving Claude $version" >> "$LOG"
53
+
54
+ for svc in "${SERVICES[@]}"; do
55
+ sqlite3 "$TCC_DB" "
56
+ INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version)
57
+ VALUES ('$svc', '$bin_path', 1, 2, 4, 1);
58
+ " 2>/dev/null
59
+ done
60
+
61
+ touch "$marker"
62
+ echo "$(date '+%Y-%m-%d %H:%M:%S') Done: Claude $version — ${#SERVICES[@]} services approved" >> "$LOG"
63
+ done
64
+ fi
65
+
66
+ # Also approve Python from NEXO's venv (if it exists)
67
+ NEXO_CODE="${NEXO_CODE:-}"
68
+ if [ -n "$NEXO_CODE" ]; then
69
+ PYTHON_BIN="$(dirname "$NEXO_CODE")/.venv/bin/python"
70
+ if [ -e "$PYTHON_BIN" ]; then
71
+ PYTHON_REAL=$(readlink -f "$PYTHON_BIN" 2>/dev/null || echo "$PYTHON_BIN")
72
+ for svc in "${SERVICES[@]}"; do
73
+ sqlite3 "$TCC_DB" "
74
+ INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version)
75
+ VALUES ('$svc', '$PYTHON_REAL', 1, 2, 4, 1);
76
+ " 2>/dev/null
77
+ done
78
+ fi
79
+ fi