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.
- package/README.md +10 -5
- package/package.json +1 -1
- package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
- package/src/cognitive.py +45 -0
- package/src/evolution_cycle.py +266 -0
- package/src/plugins/guard.py +235 -1
- package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/check-context.py +257 -0
- package/src/scripts/nexo-catchup.py +59 -5
- package/src/scripts/nexo-cognitive-decay.py +8 -0
- package/src/scripts/nexo-daily-self-audit.py +168 -183
- package/src/scripts/nexo-evolution-run.py +584 -0
- package/src/scripts/nexo-immune.py +108 -91
- package/src/scripts/nexo-learning-validator.py +226 -0
- package/src/scripts/nexo-postmortem-consolidator.py +230 -414
- package/src/scripts/nexo-sleep.py +283 -503
- package/src/scripts/nexo-synthesis.py +141 -432
- 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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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 / "
|
|
22
|
+
CLAUDE_DIR = HOME / ".nexo"
|
|
23
23
|
COORD_DIR = CLAUDE_DIR / "coordination"
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
75
|
-
LOCK_FILE.unlink()
|
|
76
|
-
except FileNotFoundError:
|
|
77
|
-
pass
|
|
62
|
+
LOCK_FILE.unlink(missing_ok=True)
|
|
78
63
|
|
|
79
64
|
|
|
80
|
-
def safe_query(
|
|
81
|
-
|
|
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(
|
|
69
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
86
70
|
conn.row_factory = sqlite3.Row
|
|
87
|
-
|
|
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
|
|
75
|
+
log(f"Query error: {e}")
|
|
93
76
|
return []
|
|
94
77
|
|
|
95
78
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
443
|
-
|
|
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
|
|
135
|
+
return data
|
|
446
136
|
|
|
447
137
|
|
|
448
|
-
|
|
138
|
+
def synthesize(data: dict) -> bool:
|
|
139
|
+
"""CLI synthesizes the daily brief."""
|
|
449
140
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
149
|
+
TODAY'S RAW DATA:
|
|
150
|
+
{data_json}
|
|
464
151
|
|
|
465
|
-
|
|
466
|
-
log("NEXO Synthesis Engine starting.")
|
|
152
|
+
Write the synthesis to {OUTPUT_FILE} with this structure:
|
|
467
153
|
|
|
468
|
-
|
|
469
|
-
sys.exit(0)
|
|
154
|
+
# NEXO Daily Synthesis — {TODAY_STR}
|
|
470
155
|
|
|
471
|
-
|
|
156
|
+
## Errors & Learnings
|
|
157
|
+
[New learnings from today — what went wrong, what was learned]
|
|
472
158
|
|
|
473
|
-
|
|
474
|
-
|
|
159
|
+
## Decisions Made
|
|
160
|
+
[Key decisions and their reasoning]
|
|
475
161
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
log("Querying databases...")
|
|
162
|
+
## Changes Deployed
|
|
163
|
+
[What was changed in production today]
|
|
479
164
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
489
|
-
|
|
169
|
+
## Weak Points (self-assessment)
|
|
170
|
+
[Where NEXO failed or could have done better today — from session diaries]
|
|
490
171
|
|
|
491
|
-
##
|
|
492
|
-
|
|
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
|
-
##
|
|
495
|
-
|
|
176
|
+
## Guard Status
|
|
177
|
+
[Areas with most learnings — where errors concentrate]
|
|
496
178
|
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
501
|
-
{s_patterns}
|
|
182
|
+
Execute without asking."""
|
|
502
183
|
|
|
503
|
-
|
|
504
|
-
{s_user_obs}
|
|
184
|
+
log("Invoking Claude CLI (sonnet) for synthesis...")
|
|
505
185
|
|
|
506
|
-
|
|
507
|
-
|
|
186
|
+
env = os.environ.copy()
|
|
187
|
+
env.pop("CLAUDECODE", None)
|
|
188
|
+
env.pop("CLAUDE_CODE", None)
|
|
508
189
|
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
514
|
-
|
|
197
|
+
if result.returncode != 0:
|
|
198
|
+
log(f"CLI error ({result.returncode}): {(result.stderr or '')[:300]}")
|
|
199
|
+
return False
|
|
515
200
|
|
|
516
|
-
|
|
517
|
-
|
|
201
|
+
log(f"Synthesis complete. Output: {len(result.stdout or '')} chars")
|
|
202
|
+
return True
|
|
518
203
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
|