nexo-brain 1.2.3 → 1.4.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 (29) hide show
  1. package/README.md +10 -5
  2. package/package.json +1 -1
  3. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  4. package/src/cognitive.py +45 -0
  5. package/src/evolution_cycle.py +266 -0
  6. package/src/plugins/guard.py +235 -1
  7. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  8. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  9. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  10. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  11. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  12. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  13. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  14. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  15. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  16. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  17. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  18. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  19. package/src/scripts/check-context.py +257 -0
  20. package/src/scripts/nexo-catchup.py +59 -5
  21. package/src/scripts/nexo-cognitive-decay.py +8 -0
  22. package/src/scripts/nexo-daily-self-audit.py +168 -183
  23. package/src/scripts/nexo-evolution-run.py +584 -0
  24. package/src/scripts/nexo-immune.py +108 -91
  25. package/src/scripts/nexo-learning-validator.py +226 -0
  26. package/src/scripts/nexo-postmortem-consolidator.py +230 -414
  27. package/src/scripts/nexo-sleep.py +283 -503
  28. package/src/scripts/nexo-synthesis.py +141 -432
  29. package/src/tools_sessions.py +20 -12
@@ -1,43 +1,35 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- NEXO Synthesis Engine — Daily intelligence brief.
3
+ NEXO Synthesis Engine v2 — Daily intelligence brief.
4
4
 
5
- Runs every 2 hours via LaunchAgent. Executes ONCE per day (internal gate).
6
- Queries nexo.db + claude-mem.db and writes ~/claude/coordination/daily-synthesis.md
5
+ Before: ~400 lines of Python concatenating SQL results into markdown sections.
6
+ Now: Collects raw data, passes to Claude CLI (sonnet) which synthesizes
7
+ with real understanding of what matters for tomorrow.
7
8
 
8
- Zero external dependencies beyond stdlib + sqlite3.
9
+ Runs every 2 hours via LaunchAgent. Executes ONCE per day (internal gate).
9
10
  """
10
11
 
11
12
  import fcntl
12
13
  import json
13
14
  import os
14
15
  import sqlite3
16
+ import subprocess
15
17
  import sys
16
- from collections import Counter, defaultdict
17
18
  from datetime import datetime, date, timedelta
18
19
  from pathlib import Path
19
20
 
20
- # ─── Paths ────────────────────────────────────────────────────────────────────
21
21
  HOME = Path.home()
22
- CLAUDE_DIR = HOME / "claude"
22
+ CLAUDE_DIR = HOME / ".nexo"
23
23
  COORD_DIR = CLAUDE_DIR / "coordination"
24
- NEXO_HOME = os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))
25
-
26
- NEXO_DB = Path(NEXO_HOME) / "nexo.db"
27
- CLAUDE_MEM_DB = HOME / ".claude-mem" / "claude-mem.db"
28
-
24
+ NEXO_DB = HOME / ".nexo" / "nexo.db"
29
25
  OUTPUT_FILE = COORD_DIR / "daily-synthesis.md"
30
- SYNTHESIS_LOG = COORD_DIR / "synthesis-log.json"
31
26
  LAST_RUN_FILE = COORD_DIR / "synthesis-last-run"
32
27
  LOCK_FILE = COORD_DIR / "synthesis.lock"
28
+ CLAUDE_CLI = HOME / ".local" / "bin" / "claude"
33
29
 
34
30
  TODAY = date.today()
35
31
  TODAY_STR = TODAY.isoformat()
36
- SEVEN_DAYS_AGO = (TODAY - timedelta(days=7)).isoformat()
37
- TOMORROW = (TODAY + timedelta(days=1)).isoformat()
38
-
39
32
 
40
- # ─── Utilities ────────────────────────────────────────────────────────────────
41
33
 
42
34
  def log(msg: str):
43
35
  ts = datetime.now().strftime("%H:%M:%S")
@@ -45,12 +37,8 @@ def log(msg: str):
45
37
 
46
38
 
47
39
  def should_run() -> bool:
48
- """Gate: run at most once per day."""
49
40
  if LAST_RUN_FILE.exists():
50
- last = LAST_RUN_FILE.read_text().strip()
51
- if last == TODAY_STR:
52
- log(f"Already ran today ({TODAY_STR}). Skipping.")
53
- return False
41
+ return LAST_RUN_FILE.read_text().strip() != TODAY_STR
54
42
  return True
55
43
 
56
44
 
@@ -64,474 +52,195 @@ def acquire_lock():
64
52
  fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
65
53
  return lock_fd
66
54
  except BlockingIOError:
67
- log("Another instance is running. Exiting.")
55
+ log("Another instance running. Exiting.")
68
56
  sys.exit(0)
69
57
 
70
58
 
71
59
  def release_lock(lock_fd):
72
60
  fcntl.flock(lock_fd, fcntl.LOCK_UN)
73
61
  lock_fd.close()
74
- try:
75
- LOCK_FILE.unlink()
76
- except FileNotFoundError:
77
- pass
62
+ LOCK_FILE.unlink(missing_ok=True)
78
63
 
79
64
 
80
- def safe_query(db_path: Path, sql: str, params=()) -> list:
81
- """Run a query against a SQLite DB, return rows or [] on any error."""
82
- if not db_path.exists():
65
+ def safe_query(sql: str, params=()) -> list:
66
+ if not NEXO_DB.exists():
83
67
  return []
84
68
  try:
85
- conn = sqlite3.connect(db_path)
69
+ conn = sqlite3.connect(str(NEXO_DB))
86
70
  conn.row_factory = sqlite3.Row
87
- cur = conn.execute(sql, params)
88
- rows = [dict(r) for r in cur.fetchall()]
71
+ rows = [dict(r) for r in conn.execute(sql, params).fetchall()]
89
72
  conn.close()
90
73
  return rows
91
74
  except Exception as e:
92
- log(f"Query error on {db_path.name}: {e}")
75
+ log(f"Query error: {e}")
93
76
  return []
94
77
 
95
78
 
96
- def truncate(text: str, max_len: int = 200) -> str:
97
- if not text:
98
- return ""
99
- text = text.strip().replace("\n", " ")
100
- return text[:max_len] + ("…" if len(text) > max_len else "")
101
-
102
-
103
- # ─── Section builders ─────────────────────────────────────────────────────────
79
+ def collect_data() -> dict:
80
+ """Collect all raw data for synthesis."""
81
+ data = {"date": TODAY_STR}
104
82
 
105
- def section_learnings() -> str:
106
- rows = safe_query(
107
- NEXO_DB,
83
+ # Today's learnings
84
+ data["learnings"] = safe_query(
108
85
  "SELECT category, title, content, reasoning FROM learnings "
109
86
  "WHERE date(created_at, 'unixepoch') = ? ORDER BY created_at DESC",
110
- (TODAY_STR,),
87
+ (TODAY_STR,)
111
88
  )
112
- if not rows:
113
- return "No new errors recorded."
114
-
115
- lines = []
116
- for r in rows:
117
- cat = r.get("category") or "general"
118
- title = r.get("title") or ""
119
- content = truncate(r.get("content") or "", 180)
120
- lines.append(f"- **[{cat}]** {title}: {content}")
121
- return "\n".join(lines)
122
-
123
-
124
- def section_decisions() -> str:
125
- # decisions table uses columns: domain, decision, alternatives, based_on, outcome
126
- rows = safe_query(
127
- NEXO_DB,
89
+
90
+ # Today's decisions
91
+ data["decisions"] = safe_query(
128
92
  "SELECT domain, decision, alternatives, based_on, outcome FROM decisions "
129
93
  "WHERE date(created_at) = ? ORDER BY created_at DESC",
130
- (TODAY_STR,),
94
+ (TODAY_STR,)
131
95
  )
132
- if not rows:
133
- return "No decisions recorded."
134
-
135
- lines = []
136
- for r in rows:
137
- domain = r.get("domain") or ""
138
- chosen = truncate(r.get("decision") or "", 160)
139
- discarded = truncate(r.get("alternatives") or "", 120)
140
- why = truncate(r.get("based_on") or "", 120)
141
- outcome = r.get("outcome") or ""
142
-
143
- line = f"- **[{domain}]** Chosen: {chosen}"
144
- if discarded:
145
- line += f"\n Discarded: {discarded}"
146
- if why:
147
- line += f"\n Why: {why}"
148
- if outcome:
149
- line += f"\n Result: {truncate(outcome, 100)}"
150
- lines.append(line)
151
- return "\n".join(lines)
152
-
153
-
154
- def section_changes() -> str:
155
- rows = safe_query(
156
- NEXO_DB,
157
- "SELECT files, what_changed, why, risks, affects FROM change_log "
96
+
97
+ # Today's changes
98
+ data["changes"] = safe_query(
99
+ "SELECT files, what_changed, why, affects, risks FROM change_log "
158
100
  "WHERE date(created_at) = ? ORDER BY created_at DESC",
159
- (TODAY_STR,),
160
- )
161
- if not rows:
162
- return "No code changes recorded."
163
-
164
- # Group by "system" (first part of first file path)
165
- by_system = defaultdict(list)
166
- for r in rows:
167
- files_raw = r.get("files") or ""
168
- # Take first file, extract top-level system name
169
- first_file = files_raw.split(",")[0].strip()
170
- parts = [p for p in first_file.replace("\\", "/").split("/") if p and p != "_public"]
171
- system = parts[0] if parts else "misc"
172
- by_system[system].append(r)
173
-
174
- lines = []
175
- for system, entries in by_system.items():
176
- lines.append(f"**{system}** ({len(entries)} change{'s' if len(entries) > 1 else ''}):")
177
- for r in entries[:3]: # cap per system
178
- what = truncate(r.get("what_changed") or "", 160)
179
- risks = truncate(r.get("risks") or "", 100)
180
- lines.append(f" - {what}")
181
- if risks:
182
- lines.append(f" ⚠ Risks: {risks}")
183
- return "\n".join(lines)
184
-
185
-
186
- def section_patterns() -> str:
187
- # Learnings by category — last 7 days
188
- learn_rows = safe_query(
189
- NEXO_DB,
190
- "SELECT category, title FROM learnings "
191
- "WHERE date(created_at, 'unixepoch') >= ? ORDER BY created_at DESC",
192
- (SEVEN_DAYS_AGO,),
193
- )
194
- # change_log — last 7 days
195
- change_rows = safe_query(
196
- NEXO_DB,
197
- "SELECT files FROM change_log WHERE date(created_at) >= ?",
198
- (SEVEN_DAYS_AGO,),
101
+ (TODAY_STR,)
199
102
  )
200
103
 
201
- total_learn = len(learn_rows)
202
- total_changes = len(change_rows)
203
-
204
- if total_learn < 3 and total_changes < 3:
205
- return "Insufficient data for pattern analysis (< 7 days)."
206
-
207
- lines = []
208
-
209
- # Categories with most learnings
210
- if learn_rows:
211
- cat_counter = Counter(r.get("category") or "general" for r in learn_rows)
212
- top_cats = cat_counter.most_common(3)
213
- lines.append(f"**Areas with most errors** (last 7d, {total_learn} learnings):")
214
- for cat, count in top_cats:
215
- lines.append(f" - {cat}: {count} {'error' if count == 1 else 'errors'}")
216
-
217
- # Systems most touched in change_log
218
- if change_rows:
219
- sys_counter: Counter = Counter()
220
- for r in change_rows:
221
- files_raw = r.get("files") or ""
222
- for f in files_raw.split(",")[:3]:
223
- f = f.strip()
224
- parts = [p for p in f.replace("\\", "/").split("/") if p and p != "_public"]
225
- if parts:
226
- sys_counter[parts[0]] += 1
227
- top_sys = sys_counter.most_common(3)
228
- lines.append(f"**Most touched systems** (last 7d, {total_changes} changes):")
229
- for sys_name, count in top_sys:
230
- lines.append(f" - {sys_name}: {count} {'modification' if count == 1 else 'modifications'}")
231
-
232
- # Recurring error patterns — categories with learnings on 3+ different days
233
- if learn_rows:
234
- # Get daily breakdown per category
235
- daily_cats = safe_query(
236
- NEXO_DB,
237
- "SELECT category, date(created_at, 'unixepoch') as day "
238
- "FROM learnings WHERE date(created_at, 'unixepoch') >= ? "
239
- "GROUP BY category, day",
240
- (SEVEN_DAYS_AGO,),
241
- )
242
- if daily_cats:
243
- cat_days = Counter(r.get("category") or "general" for r in daily_cats)
244
- recurring = [(c, d) for c, d in cat_days.items() if d >= 3]
245
- if recurring:
246
- lines.append("**Categories with recurring errors** (3+ different days):")
247
- for cat, days in sorted(recurring, key=lambda x: -x[1]):
248
- lines.append(f" - {cat}: errors on {days} days — weak point")
249
-
250
- return "\n".join(lines) if lines else "No significant patterns detected."
251
-
252
-
253
- def section_manana() -> str:
254
- lines = []
255
-
256
- # Reminders due <= tomorrow, PENDIENTE
257
- rem_rows = safe_query(
258
- NEXO_DB,
259
- "SELECT id, date, description, category FROM reminders "
260
- "WHERE status LIKE 'PENDIENTE%' AND date IS NOT NULL AND date <= ? "
261
- "ORDER BY date ASC",
262
- (TOMORROW,),
263
- )
264
- if rem_rows:
265
- lines.append("### Overdue/tomorrow reminders")
266
- for r in rem_rows:
267
- d = r.get("date") or ""
268
- cat = r.get("category") or ""
269
- desc = truncate(r.get("description") or "", 150)
270
- overdue = " ⚠ OVERDUE" if d and d < TODAY_STR else ""
271
- lines.append(f"- [{d}]{overdue} {desc}" + (f" ({cat})" if cat else ""))
272
- else:
273
- lines.append("### Reminders\nNone overdue or due tomorrow.")
274
-
275
- # Followups due <= tomorrow, PENDIENTE
276
- fol_rows = safe_query(
277
- NEXO_DB,
278
- "SELECT id, date, description FROM followups "
279
- "WHERE status = 'PENDIENTE' AND date IS NOT NULL AND date <= ? "
280
- "ORDER BY date ASC",
281
- (TOMORROW,),
282
- )
283
- if fol_rows:
284
- lines.append("### Overdue/tomorrow followups")
285
- for r in fol_rows:
286
- d = r.get("date") or ""
287
- desc = truncate(r.get("description") or "", 150)
288
- overdue = " ⚠ OVERDUE" if d and d < TODAY_STR else ""
289
- lines.append(f"- [{d}]{overdue} {desc}")
290
- else:
291
- lines.append("### Followups\nNone overdue or due tomorrow.")
292
-
293
- # Last 3 session diary entries — pending + next_session_context
294
- diary_rows = safe_query(
295
- NEXO_DB,
296
- "SELECT domain, pending, context_next, created_at FROM session_diary "
297
- "ORDER BY created_at DESC LIMIT 3",
298
- )
299
- if diary_rows:
300
- lines.append("### Active context (recent sessions)")
301
- for r in diary_rows:
302
- domain = r.get("domain") or "general"
303
- pending = truncate(r.get("pending") or "", 200)
304
- nxt = truncate(r.get("context_next") or "", 200)
305
- ts = r.get("created_at") or ""
306
- if pending or nxt:
307
- lines.append(f"**[{domain}]** ({ts[:16]}):")
308
- if pending:
309
- lines.append(f" Pending: {pending}")
310
- if nxt:
311
- lines.append(f" For next session: {nxt}")
312
-
313
- return "\n".join(lines) if lines else "No items for tomorrow."
314
-
315
-
316
- def section_autoevaluacion() -> str:
317
- diary_rows = safe_query(
318
- NEXO_DB,
319
- "SELECT mental_state, user_signals, self_critique, summary, created_at FROM session_diary "
104
+ # Session diaries (summaries + mental_state)
105
+ data["diaries"] = safe_query(
106
+ "SELECT summary, self_critique, mental_state, user_signals FROM session_diary "
320
107
  "WHERE date(created_at) = ? ORDER BY created_at DESC",
321
- (TODAY_STR,),
108
+ (TODAY_STR,)
322
109
  )
323
- if not diary_rows:
324
- return "No session diaries recorded today."
325
-
326
- lines = []
327
-
328
- # Self-critique section (NEW — most important)
329
- all_critiques = []
330
- for r in diary_rows:
331
- sc = r.get("self_critique") or ""
332
- if sc.strip() and not sc.strip().lower().startswith("no self-critique"):
333
- all_critiques.append(truncate(sc, 300))
334
-
335
- if all_critiques:
336
- lines.append(f"**SELF-CRITIQUES ({len(all_critiques)} sessions with detected failures):**")
337
- for c in all_critiques[:5]:
338
- lines.append(f" - {c}")
339
- lines.append("**ACTION:** These self-critiques should inform tomorrow's behavior. If a pattern repeats 3+ days, the nightly consolidator will promote it to permanent memory.")
340
- lines.append("")
341
-
342
- # user_signals patterns
343
- all_signals = []
344
- mental_states = []
345
- for r in diary_rows:
346
- sig = r.get("user_signals") or ""
347
- if sig.strip():
348
- all_signals.append(truncate(sig, 200))
349
- ms = r.get("mental_state") or ""
350
- if ms.strip():
351
- mental_states.append(truncate(ms, 200))
352
-
353
- if user_signals_text := "\n".join(f" - {s}" for s in all_signals[:3] if s):
354
- lines.append(f"**User signals:**\n{user_signals_text}")
355
-
356
- if mental_states:
357
- lines.append(f"**Session mental states:**")
358
- for ms in mental_states[:2]:
359
- lines.append(f" - {ms}")
360
-
361
- # Derive what to do differently based on signal analysis
362
- if all_signals:
363
- # Detect repeated corrections
364
- correction_words = ["corrig", "frustrat", "don't understand", "demand", "repeat",
365
- "shouldn't", "why not", "again", "tiring",
366
- "always wait", "reactive", "not proactive"]
367
- correction_count = sum(
368
- 1 for s in all_signals
369
- if any(w in s.lower() for w in correction_words)
370
- )
371
- if correction_count >= 2:
372
- lines.append(f"**ALERT:** User corrected {correction_count} times today — review what is repeating.")
373
- lines.append("**For tomorrow:** Review previous signals before acting.")
374
- elif not diary_rows:
375
- lines.append("**For tomorrow:** Remember to write diary before closing session.")
376
-
377
- # Check for postmortem daily summary
378
- postmortem_file = COORD_DIR / "postmortem-daily.md"
379
- if postmortem_file.exists():
380
- pm_content = postmortem_file.read_text().strip()
381
- if "Promovido a memoria permanente" in pm_content:
382
- lines.append("")
383
- lines.append("**NEW PERMANENT RULES (generated last night by the consolidator):**")
384
- for line in pm_content.split("\n"):
385
- if line.startswith("- ") and "Promovido" not in line:
386
- lines.append(f" {line}")
387
-
388
- return "\n".join(lines) if lines else "No self-evaluation data."
389
-
390
-
391
- def section_user_observer() -> str:
392
- """Track user's patterns: forgotten ideas, abandoned topics, recurring requests."""
393
- lines = []
394
-
395
- # 1. Reminders without dates (ideas that accumulate without agenda)
396
- no_date = safe_query(
397
- NEXO_DB,
398
- "SELECT id, description FROM reminders "
399
- "WHERE date IS NULL AND status LIKE 'PENDIENTE%' ORDER BY rowid",
110
+
111
+ # Overdue reminders
112
+ data["overdue_reminders"] = safe_query(
113
+ "SELECT id, title, due_date FROM reminders "
114
+ "WHERE status='PENDING' AND due_date <= ? ORDER BY due_date",
115
+ (TODAY_STR,)
400
116
  )
401
- if no_date:
402
- lines.append(f"**Ideas sin agenda:** {len(no_date)} reminders sin fecha")
403
- # Show oldest 3 as examples
404
- for r in no_date[:3]:
405
- desc = truncate(r.get("description") or "", 80)
406
- lines.append(f" - {r.get('id')}: {desc}")
407
- if len(no_date) > 3:
408
- lines.append(f" - ... and {len(no_date) - 3} more")
409
-
410
- # 2. Followups waiting on user or external responses
411
- waiting = safe_query(
412
- NEXO_DB,
413
- "SELECT id, description, date FROM followups "
414
- "WHERE status = 'PENDIENTE' "
415
- "AND (description LIKE '%respuesta%' "
416
- " OR description LIKE '%preguntar%' OR description LIKE '%confirme%' "
417
- " OR description LIKE '%decidió%') "
418
- "ORDER BY date",
117
+
118
+ # Pending followups
119
+ data["pending_followups"] = safe_query(
120
+ "SELECT id, title, description, due_date FROM followups "
121
+ "WHERE status='pending' ORDER BY due_date"
419
122
  )
420
- if waiting:
421
- lines.append(f"**Waiting for user or third-party response/decision:** {len(waiting)}")
422
- for r in waiting[:5]:
423
- d = r.get("date") or "no date"
424
- desc = truncate(r.get("description") or "", 100)
425
- lines.append(f" - {r.get('id')} ({d}): {desc}")
426
-
427
- # 3. Overdue reminders that keep getting postponed (same reminder, multiple updates)
428
- # Detect by looking at reminders with dates far past
429
- stale = safe_query(
430
- NEXO_DB,
431
- "SELECT id, description, date FROM reminders "
432
- "WHERE status LIKE 'PENDIENTE%' AND date IS NOT NULL AND date < ? "
433
- "ORDER BY date ASC LIMIT 5",
434
- (TODAY_STR,),
123
+
124
+ # Guard stats
125
+ data["guard_stats"] = safe_query(
126
+ "SELECT category, COUNT(*) as cnt FROM learnings WHERE status='active' "
127
+ "GROUP BY category ORDER BY cnt DESC LIMIT 10"
435
128
  )
436
- if stale:
437
- lines.append(f"**Overdue reminders not attended:**")
438
- for r in stale:
439
- desc = truncate(r.get("description") or "", 80)
440
- lines.append(f" - {r.get('id')} (overdue since {r.get('date')}): {desc}")
441
129
 
442
- if not lines:
443
- return "No observations on user patterns."
130
+ # Postmortem daily (if exists)
131
+ pm_file = COORD_DIR / "postmortem-daily.md"
132
+ if pm_file.exists():
133
+ data["postmortem_summary"] = pm_file.read_text()[:2000]
444
134
 
445
- return "\n".join(lines)
135
+ return data
446
136
 
447
137
 
448
- # ─── Log history ──────────────────────────────────────────────────────────────
138
+ def synthesize(data: dict) -> bool:
139
+ """CLI synthesizes the daily brief."""
449
140
 
450
- def append_synthesis_log(entry: dict):
451
- log_data = []
452
- if SYNTHESIS_LOG.exists():
453
- try:
454
- log_data = json.loads(SYNTHESIS_LOG.read_text())
455
- except Exception:
456
- log_data = []
457
- log_data.append(entry)
458
- # Keep last 30 entries
459
- log_data = log_data[-30:]
460
- SYNTHESIS_LOG.write_text(json.dumps(log_data, ensure_ascii=False, indent=2))
141
+ data_json = json.dumps(data, ensure_ascii=False, indent=1)
142
+ if len(data_json) > 15000:
143
+ data_json = data_json[:15000] + "\n... (truncated)"
461
144
 
145
+ prompt = f"""You are NEXO's synthesis engine. Write the daily intelligence brief for tomorrow's
146
+ startup. This file is read by NEXO at the beginning of each session to understand
147
+ what happened today and what to focus on tomorrow.
462
148
 
463
- # ─── Main ─────────────────────────────────────────────────────────────────────
149
+ TODAY'S RAW DATA:
150
+ {data_json}
464
151
 
465
- def main():
466
- log("NEXO Synthesis Engine starting.")
152
+ Write the synthesis to {OUTPUT_FILE} with this structure:
467
153
 
468
- if not should_run():
469
- sys.exit(0)
154
+ # NEXO Daily Synthesis — {TODAY_STR}
470
155
 
471
- lock_fd = acquire_lock()
156
+ ## Errors & Learnings
157
+ [New learnings from today — what went wrong, what was learned]
472
158
 
473
- try:
474
- COORD_DIR.mkdir(parents=True, exist_ok=True)
159
+ ## Decisions Made
160
+ [Key decisions and their reasoning]
475
161
 
476
- now = datetime.now()
477
- ts = now.strftime("%Y-%m-%d %H:%M")
478
- log("Querying databases...")
162
+ ## Changes Deployed
163
+ [What was changed in production today]
479
164
 
480
- s_learnings = section_learnings()
481
- s_decisions = section_decisions()
482
- s_changes = section_changes()
483
- s_patterns = section_patterns()
484
- s_manana = section_manana()
485
- s_autoeval = section_autoevaluacion()
486
- s_user_obs = section_user_observer()
165
+ ## the user — Observations
166
+ [Patterns in the user's behavior: frustrations, pending decisions, ideas without
167
+ deadlines, topics he started but didn't close. This is NEXO's peripheral vision.]
487
168
 
488
- md = f"""# NEXO Daily Synthesis — {TODAY_STR}
489
- Generated at {ts}
169
+ ## Weak Points (self-assessment)
170
+ [Where NEXO failed or could have done better today — from session diaries]
490
171
 
491
- ## Errors and Lessons (today)
492
- {s_learnings}
172
+ ## Tomorrow's Context
173
+ [What the next session needs to know: pending followups, overdue reminders,
174
+ in-progress tasks, things to verify]
493
175
 
494
- ## Decisions Made
495
- {s_decisions}
176
+ ## Guard Status
177
+ [Areas with most learnings — where errors concentrate]
496
178
 
497
- ## Systems Touched
498
- {s_changes}
179
+ Be concise. Each section 3-8 bullet points max. Focus on what CHANGES BEHAVIOR,
180
+ not what merely happened. If a section has nothing, write "Nothing notable."
499
181
 
500
- ## Patterns Detected
501
- {s_patterns}
182
+ Execute without asking."""
502
183
 
503
- ## User Observations
504
- {s_user_obs}
184
+ log("Invoking Claude CLI (sonnet) for synthesis...")
505
185
 
506
- ## Tomorrow
507
- {s_manana}
186
+ env = os.environ.copy()
187
+ env.pop("CLAUDECODE", None)
188
+ env.pop("CLAUDE_CODE", None)
508
189
 
509
- ## Self-Evaluation
510
- {s_autoeval}
511
- """
190
+ try:
191
+ result = subprocess.run(
192
+ [str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
193
+ "--allowedTools", "Read,Write,Edit,Glob,Grep"],
194
+ capture_output=True, text=True, timeout=180, env=env
195
+ )
512
196
 
513
- OUTPUT_FILE.write_text(md, encoding="utf-8")
514
- log(f"Written: {OUTPUT_FILE}")
197
+ if result.returncode != 0:
198
+ log(f"CLI error ({result.returncode}): {(result.stderr or '')[:300]}")
199
+ return False
515
200
 
516
- line_count = len(md.splitlines())
517
- log(f"Output: {line_count} lines.")
201
+ log(f"Synthesis complete. Output: {len(result.stdout or '')} chars")
202
+ return True
518
203
 
519
- # Log history
520
- append_synthesis_log({
521
- "date": TODAY_STR,
522
- "generated_at": ts,
523
- "lines": line_count,
524
- "learnings_today": s_learnings.count("\n- ") + (1 if s_learnings.startswith("- ") else 0),
525
- })
204
+ except subprocess.TimeoutExpired:
205
+ log("CLI timed out (180s)")
206
+ return False
207
+ except Exception as e:
208
+ log(f"Exception: {e}")
209
+ return False
526
210
 
527
- mark_done()
528
- log("Done.")
529
211
 
530
- except Exception as e:
531
- log(f"Fatal error: {e}")
532
- import traceback
533
- traceback.print_exc()
534
- sys.exit(1)
212
+ def main():
213
+ if not should_run():
214
+ log(f"Already ran today ({TODAY_STR}). Skipping.")
215
+ return
216
+
217
+ lock_fd = acquire_lock()
218
+ try:
219
+ log(f"=== NEXO Synthesis v2 — {TODAY_STR} ===")
220
+
221
+ data = collect_data()
222
+ log(f"Collected: {len(data.get('learnings', []))} learnings, "
223
+ f"{len(data.get('decisions', []))} decisions, "
224
+ f"{len(data.get('changes', []))} changes, "
225
+ f"{len(data.get('diaries', []))} diaries")
226
+
227
+ success = synthesize(data)
228
+
229
+ if success:
230
+ mark_done()
231
+ log("Synthesis v2 complete.")
232
+ else:
233
+ log("Synthesis failed — will retry next trigger.")
234
+
235
+ # Register for catch-up
236
+ try:
237
+ state_file = HOME / ".nexo" / "operations" / ".catchup-state.json"
238
+ st = json.loads(state_file.read_text()) if state_file.exists() else {}
239
+ st["synthesis"] = datetime.now().isoformat()
240
+ state_file.write_text(json.dumps(st, indent=2))
241
+ except Exception:
242
+ pass
243
+
535
244
  finally:
536
245
  release_lock(lock_fd)
537
246