nexo-brain 0.2.1 → 0.3.2

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.
@@ -1,5 +1,13 @@
1
1
  """Cognitive Memory plugin — RAG retrieval over NEXO's Atkinson-Shiffrin memory stores."""
2
2
 
3
+ import sys
4
+ import os
5
+
6
+ # Ensure site-packages is in path for numpy/fastembed
7
+ _site = "/opt/homebrew/lib/python{}.{}/site-packages".format(sys.version_info.major, sys.version_info.minor)
8
+ if os.path.isdir(_site) and _site not in sys.path:
9
+ sys.path.insert(0, _site)
10
+
3
11
  import cognitive
4
12
 
5
13
 
@@ -10,6 +18,9 @@ def handle_cognitive_retrieve(
10
18
  stores: str = "both",
11
19
  source_type: str = "",
12
20
  domain: str = "",
21
+ include_archived: bool = False,
22
+ use_hyde: bool = False,
23
+ spreading_depth: int = 0,
13
24
  ) -> str:
14
25
  """RAG query over cognitive memory (STM + LTM). Triggers rehearsal on retrieved memories.
15
26
 
@@ -19,7 +30,10 @@ def handle_cognitive_retrieve(
19
30
  min_score: Minimum cosine similarity score (default 0.5)
20
31
  stores: Which store to search — "both", "stm", or "ltm" (default "both")
21
32
  source_type: Filter by source type e.g. "change", "learning", "diary" (default: all)
22
- domain: Filter by domain e.g. "infrastructure", "general" (default: all)
33
+ domain: Filter by domain e.g. "wazion", "shopify" (default: all)
34
+ include_archived: If True, also search archived memories (default False)
35
+ use_hyde: If True, use HyDE query expansion — embeds 3-5 query variants and searches with centroid. Better recall for conceptual queries. (default False)
36
+ spreading_depth: If >0, boost co-activated neighbors (memories frequently retrieved together). 1=direct neighbors only. (default 0)
23
37
  """
24
38
  if not query or not query.strip():
25
39
  return "ERROR: query is required."
@@ -32,6 +46,9 @@ def handle_cognitive_retrieve(
32
46
  exclude_dormant=True,
33
47
  rehearse=True,
34
48
  source_type_filter=source_type,
49
+ include_archived=include_archived,
50
+ use_hyde=use_hyde,
51
+ spreading_depth=spreading_depth,
35
52
  )
36
53
 
37
54
  # Apply domain filter post-search (cognitive.search doesn't filter by domain natively)
@@ -39,7 +56,12 @@ def handle_cognitive_retrieve(
39
56
  results = [r for r in results if r.get("domain", "") == domain]
40
57
 
41
58
  formatted = cognitive.format_results(results)
42
- header = f"COGNITIVE RETRIEVE — query: '{query}' | {len(results)} results (stores={stores}, min_score={min_score})\n\n"
59
+ mode_parts = [f"stores={stores}", f"min_score={min_score}"]
60
+ if use_hyde:
61
+ mode_parts.append("hyde=ON")
62
+ if spreading_depth > 0:
63
+ mode_parts.append(f"spreading={spreading_depth}")
64
+ header = f"COGNITIVE RETRIEVE — query: '{query}' | {len(results)} results ({', '.join(mode_parts)})\n\n"
43
65
  return header + formatted
44
66
 
45
67
 
@@ -68,6 +90,21 @@ def handle_cognitive_stats() -> str:
68
90
  for domain, cnt in stats["top_domains_ltm"]:
69
91
  lines.append(f" {domain}: {cnt}")
70
92
 
93
+ if "quarantine" in stats:
94
+ q = stats["quarantine"]
95
+ lines.append(f" Quarantine pending: {q.get('pending', 0)}")
96
+ lines.append(f" Quarantine promoted: {q.get('promoted', 0)}")
97
+ lines.append(f" Quarantine rejected: {q.get('rejected', 0)}")
98
+ lines.append(f" Quarantine expired: {q.get('expired', 0)}")
99
+
100
+ if "prediction_error_gate" in stats:
101
+ g = stats["prediction_error_gate"]
102
+ lines.append(" PE Gate (session):")
103
+ lines.append(f" Accepted (novel): {g['accepted_novel']}")
104
+ lines.append(f" Accepted (refine): {g['accepted_refinement']}")
105
+ lines.append(f" Rejected (redundant): {g['rejected']}")
106
+ lines.append(f" Rejection rate: {g['rejection_rate_pct']}%")
107
+
71
108
  return "\n".join(lines)
72
109
 
73
110
 
@@ -104,6 +141,12 @@ def handle_cognitive_inspect(memory_id: int, store: str = "ltm") -> str:
104
141
  f" last_accessed: {row['last_accessed']}",
105
142
  ]
106
143
 
144
+ # Lifecycle state
145
+ lifecycle = row["lifecycle_state"] or "active"
146
+ lines.append(f" lifecycle: {lifecycle}")
147
+ if row["snooze_until"]:
148
+ lines.append(f" snooze_until: {row['snooze_until']}")
149
+
107
150
  if store == "ltm":
108
151
  dormant_label = "YES" if row["is_dormant"] else "no"
109
152
  lines.append(f" dormant: {dormant_label}")
@@ -120,7 +163,7 @@ def handle_cognitive_inspect(memory_id: int, store: str = "ltm") -> str:
120
163
 
121
164
 
122
165
  def handle_cognitive_metrics(days: int = 7) -> str:
123
- """Cognitive memory performance metrics.
166
+ """Cognitive memory performance metrics (spec section 9).
124
167
 
125
168
  Returns retrieval relevance %, repeat error rate, score distribution,
126
169
  and whether multilingual model switch is recommended.
@@ -155,7 +198,7 @@ def handle_cognitive_metrics(days: int = 7) -> str:
155
198
 
156
199
  if metrics["needs_multilingual"]:
157
200
  lines.append("")
158
- lines.append("RECOMMENDATION: Switch to multilingual model (intfloat/multilingual-e5-small)")
201
+ lines.append("RECOMMENDATION: Switch to multilingual model (intfloat/multilingual-e5-small)")
159
202
  lines.append(f" Reason: relevance {metrics['retrieval_relevance_pct']}% < 70% with {metrics['total_retrievals']}+ retrievals")
160
203
 
161
204
  if repeats["duplicates"]:
@@ -165,17 +208,27 @@ def handle_cognitive_metrics(days: int = 7) -> str:
165
208
  lines.append(f" [{d['score']}] STM#{d['new_stm_id']}: {d['new_content'][:60]}...")
166
209
  lines.append(f" ≈ LTM#{d['ltm_id']}: {d['ltm_content'][:60]}...")
167
210
 
211
+ # Prediction Error Gate stats
212
+ gate = cognitive.get_gate_stats()
213
+ if gate["total_evaluated"] > 0:
214
+ lines.append("")
215
+ lines.append("Prediction Error Gate (session):")
216
+ lines.append(f" Novel accepted: {gate['accepted_novel']}")
217
+ lines.append(f" Refinements: {gate['accepted_refinement']}")
218
+ lines.append(f" Rejected redundant: {gate['rejected']}")
219
+ lines.append(f" Rejection rate: {gate['rejection_rate_pct']}%")
220
+
168
221
  return "\n".join(lines)
169
222
 
170
223
 
171
224
  def handle_cognitive_sentiment(text: str) -> str:
172
- """Detect user sentiment from their text. Returns mood, intensity, and guidance.
225
+ """Detect the user's sentiment from his text. Returns mood, intensity, and guidance.
173
226
 
174
227
  Call this with the user's recent message to adapt NEXO's tone and behavior.
175
228
  Also logs the sentiment for historical tracking.
176
229
 
177
230
  Args:
178
- text: User's recent message or instruction
231
+ text: the user's recent message or instruction
179
232
  """
180
233
  result = cognitive.log_sentiment(text)
181
234
  trust = cognitive.get_trust_score()
@@ -241,27 +294,27 @@ def handle_cognitive_trust(event: str = '', context: str = '', delta: float = No
241
294
  def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
242
295
  """Detect cognitive dissonance: find established memories that conflict with a new instruction.
243
296
 
244
- Use BEFORE applying a new preference or rule that might contradict existing knowledge.
245
- If conflicts found, verbalize them and ask to resolve.
297
+ Use BEFORE applying a new preference or rule from the user that might contradict
298
+ existing knowledge. If conflicts found, verbalize them and ask the user to resolve.
246
299
 
247
300
  Args:
248
301
  instruction: The new instruction or preference to check against LTM
249
302
  force: If True, skip discussion — execute instruction, auto-resolve all conflicts as
250
- 'exception', and flag for review.
303
+ 'exception', and flag for review in the nocturnal process (23:30).
251
304
  """
252
305
  conflicts = cognitive.detect_dissonance(instruction)
253
306
  if not conflicts:
254
307
  return f"No dissonance detected. Instruction '{instruction[:80]}' is consistent with existing LTM."
255
308
 
256
309
  if force:
257
- # Auto-resolve all as exceptions
310
+ # Auto-resolve all as exceptions, log for nocturnal review
258
311
  for c in conflicts:
259
312
  cognitive.resolve_dissonance(
260
313
  c["memory_id"], "exception",
261
- f"[FORCE] {instruction[:200]} — auto-exception, pending review"
314
+ f"[FORCE] {instruction[:200]} — auto-exception, pending nocturnal review"
262
315
  )
263
316
  return (f"FORCE: {len(conflicts)} conflicts auto-resolved as exceptions. "
264
- f"Instruction executed. Flagged for review.")
317
+ f"Instruction executed. Flagged for review at 23:30.")
265
318
 
266
319
  lines = [
267
320
  f"COGNITIVE DISSONANCE DETECTED — {len(conflicts)} conflicting memories:",
@@ -275,7 +328,7 @@ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
275
328
  lines.append("")
276
329
 
277
330
  lines.append("RESOLVE with nexo_cognitive_resolve, or use force=True to skip:")
278
- lines.append(" - 'paradigm_shift': Permanent preference change.")
331
+ lines.append(" - 'paradigm_shift': the user changed his mind permanently.")
279
332
  lines.append(" - 'exception': One-time override. Old memory stays.")
280
333
  lines.append(" - 'override': Old memory was wrong.")
281
334
 
@@ -283,7 +336,7 @@ def handle_cognitive_dissonance(instruction: str, force: bool = False) -> str:
283
336
 
284
337
 
285
338
  def handle_cognitive_resolve(memory_id: int, resolution: str, context: str = '') -> str:
286
- """Resolve a cognitive dissonance.
339
+ """Resolve a cognitive dissonance by applying the user's decision.
287
340
 
288
341
  Args:
289
342
  memory_id: The LTM memory ID from the dissonance detection
@@ -293,13 +346,219 @@ def handle_cognitive_resolve(memory_id: int, resolution: str, context: str = '')
293
346
  return cognitive.resolve_dissonance(memory_id, resolution, context)
294
347
 
295
348
 
349
+ def handle_cognitive_pin(memory_id: int, store: str = "auto") -> str:
350
+ """Pin a memory so it NEVER decays and gets boosted in search results (+0.2 similarity).
351
+
352
+ Args:
353
+ memory_id: Integer ID of the memory to pin
354
+ store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
355
+ """
356
+ return cognitive.set_lifecycle(memory_id, "pinned", store)
357
+
358
+
359
+ def handle_cognitive_snooze(memory_id: int, until_date: str, store: str = "auto") -> str:
360
+ """Snooze a memory — hidden from searches until the given date, then auto-restores to active.
361
+
362
+ Args:
363
+ memory_id: Integer ID of the memory to snooze
364
+ until_date: Date to restore the memory (YYYY-MM-DD format)
365
+ store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
366
+ """
367
+ return cognitive.set_lifecycle(memory_id, "snoozed", store, snooze_until=until_date)
368
+
369
+
370
+ def handle_cognitive_archive(memory_id: int, store: str = "auto") -> str:
371
+ """Archive a memory — stored but excluded from normal searches. Can be restored later.
372
+
373
+ Args:
374
+ memory_id: Integer ID of the memory to archive
375
+ store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
376
+ """
377
+ return cognitive.set_lifecycle(memory_id, "archived", store)
378
+
379
+
380
+ def handle_cognitive_restore(memory_id: int, store: str = "auto") -> str:
381
+ """Restore a memory to active state (from pinned, snoozed, or archived).
382
+
383
+ Args:
384
+ memory_id: Integer ID of the memory to restore
385
+ store: Which store — "stm", "ltm", or "auto" (tries both, default "auto")
386
+ """
387
+ return cognitive.set_lifecycle(memory_id, "active", store)
388
+
389
+
390
+ def handle_cognitive_quarantine_list(status: str = "pending", limit: int = 20) -> str:
391
+ """List quarantine queue items. Shows memories awaiting promotion to STM.
392
+
393
+ Args:
394
+ status: Filter — 'pending', 'promoted', 'rejected', 'expired', or 'all' (default 'pending')
395
+ limit: Max items to return (default 20)
396
+ """
397
+ items = cognitive.quarantine_list(status=status, limit=limit)
398
+ stats = cognitive.quarantine_stats()
399
+
400
+ lines = [
401
+ f"QUARANTINE QUEUE — {stats['pending']} pending | {stats['promoted']} promoted | {stats['rejected']} rejected | {stats['expired']} expired",
402
+ f"Showing: {status} (limit {limit})",
403
+ "",
404
+ ]
405
+
406
+ if not items:
407
+ lines.append("No items found.")
408
+ else:
409
+ for item in items:
410
+ lines.append(f" #{item['id']} [{item['status']}] source={item['source']} type={item['source_type']} domain={item['domain'] or '-'}")
411
+ lines.append(f" confidence={item['confidence']:.1f} checks={item['promotion_checks']} created={item['created_at'][:16]}")
412
+ if item['promoted_at']:
413
+ lines.append(f" promoted_at={item['promoted_at'][:16]}")
414
+ lines.append(f" {item['content']}")
415
+ lines.append("")
416
+
417
+ return "\n".join(lines)
418
+
419
+
420
+ def handle_cognitive_quarantine_promote(quarantine_id: int) -> str:
421
+ """Manually promote a quarantine item to STM, bypassing the automatic promotion policy.
422
+
423
+ Args:
424
+ quarantine_id: ID of the quarantine entry to promote
425
+ """
426
+ return cognitive.quarantine_promote(quarantine_id)
427
+
428
+
429
+ def handle_cognitive_quarantine_reject(quarantine_id: int, reason: str = "") -> str:
430
+ """Manually reject a quarantine item.
431
+
432
+ Args:
433
+ quarantine_id: ID of the quarantine entry to reject
434
+ reason: Optional reason for rejection
435
+ """
436
+ return cognitive.quarantine_reject(quarantine_id, reason)
437
+
438
+
439
+ def handle_cognitive_quarantine_process() -> str:
440
+ """Run the quarantine promotion cycle. Evaluates all pending items against the promotion policy.
441
+
442
+ Promotion rules:
443
+ - source='user_direct' → already promoted at ingest
444
+ - source='inferred' + second occurrence found → promote
445
+ - source='agent_observation' + >24h old + no LTM contradiction → promote
446
+ - Contradicts LTM (cosine >0.8) → reject
447
+ - >7 days old → expire
448
+ """
449
+ result = cognitive.process_quarantine()
450
+ lines = [
451
+ "QUARANTINE PROCESSING COMPLETE",
452
+ f" Promoted: {result['promoted']}",
453
+ f" Rejected: {result['rejected']}",
454
+ f" Expired: {result['expired']}",
455
+ f" Still pending: {result['still_pending']}",
456
+ f" Total: {result['total_processed']}",
457
+ ]
458
+ return "\n".join(lines)
459
+
460
+
461
+ # ============================================================================
462
+ # Prospective Memory trigger handlers (Feature 3)
463
+ # ============================================================================
464
+
465
+ def handle_cognitive_trigger_create(pattern: str, action: str, context: str = "") -> str:
466
+ """Create a prospective memory trigger — fires when text matches pattern.
467
+
468
+ Args:
469
+ pattern: Keywords to match (case-insensitive, comma-separated for OR matching)
470
+ action: What to do / remind about when the trigger fires
471
+ context: Optional context about why this trigger was created
472
+ """
473
+ trigger_id = cognitive.create_trigger(pattern, action, context)
474
+ return f"Trigger #{trigger_id} created — armed. Pattern: '{pattern}' | Action: '{action}'"
475
+
476
+
477
+ def handle_cognitive_trigger_list(status: str = "armed") -> str:
478
+ """List prospective memory triggers.
479
+
480
+ Args:
481
+ status: Filter — 'armed' (active, waiting), 'fired' (already triggered), 'all'
482
+ """
483
+ triggers = cognitive.list_triggers(status)
484
+ if not triggers:
485
+ return f"No {status} triggers found."
486
+
487
+ lines = [f"PROSPECTIVE TRIGGERS ({status}) — {len(triggers)} total", ""]
488
+ for t in triggers:
489
+ status_icon = "+" if t["status"] == "armed" else "x"
490
+ lines.append(f" [{status_icon}] #{t['id']} pattern='{t['trigger_pattern']}'")
491
+ lines.append(f" action: {t['action']}")
492
+ if t.get("context"):
493
+ lines.append(f" context: {t['context']}")
494
+ lines.append(f" created: {t['created_at'][:16]}")
495
+ if t.get("fired_at"):
496
+ lines.append(f" fired: {t['fired_at'][:16]}")
497
+ lines.append("")
498
+
499
+ return "\n".join(lines)
500
+
501
+
502
+ def handle_cognitive_trigger_check(text: str, use_semantic: bool = False) -> str:
503
+ """Check text against all armed triggers and fire matching ones.
504
+
505
+ Args:
506
+ text: Text to check against triggers (e.g. user message, heartbeat context)
507
+ use_semantic: Also use embedding similarity (slower but catches conceptual matches)
508
+ """
509
+ fired = cognitive.check_triggers(text, use_semantic=use_semantic)
510
+ if not fired:
511
+ return "No triggers fired."
512
+
513
+ lines = [f"TRIGGERS FIRED: {len(fired)}", ""]
514
+ for t in fired:
515
+ lines.append(f" #{t['id']} [{t['match_type']}] pattern='{t['pattern']}'")
516
+ lines.append(f" ACTION: {t['action']}")
517
+ if t.get("context"):
518
+ lines.append(f" context: {t['context']}")
519
+ lines.append("")
520
+
521
+ return "\n".join(lines)
522
+
523
+
524
+ def handle_cognitive_trigger_delete(trigger_id: int) -> str:
525
+ """Delete a prospective memory trigger.
526
+
527
+ Args:
528
+ trigger_id: ID of the trigger to delete
529
+ """
530
+ return cognitive.delete_trigger(trigger_id)
531
+
532
+
533
+ def handle_cognitive_trigger_rearm(trigger_id: int) -> str:
534
+ """Re-arm a fired trigger so it can fire again.
535
+
536
+ Args:
537
+ trigger_id: ID of the trigger to re-arm
538
+ """
539
+ return cognitive.rearm_trigger(trigger_id)
540
+
541
+
296
542
  TOOLS = [
297
543
  (handle_cognitive_retrieve, "nexo_cognitive_retrieve", "RAG query over cognitive memory (STM+LTM). Triggers rehearsal on retrieved results."),
298
- (handle_cognitive_stats, "nexo_cognitive_stats", "Cognitive memory system metrics: STM/LTM counts, strengths, retrieval stats"),
544
+ (handle_cognitive_stats, "nexo_cognitive_stats", "Cognitive memory system metrics: STM/LTM counts, strengths, retrieval stats, quarantine counts"),
299
545
  (handle_cognitive_inspect, "nexo_cognitive_inspect", "Inspect a specific memory by ID (debug). Does NOT trigger rehearsal."),
300
- (handle_cognitive_metrics, "nexo_cognitive_metrics", "Performance metrics: retrieval relevance %, repeat error rate, multilingual recommendation"),
546
+ (handle_cognitive_metrics, "nexo_cognitive_metrics", "Performance metrics: retrieval relevance %, repeat error rate, multilingual recommendation (spec section 9)"),
301
547
  (handle_cognitive_dissonance, "nexo_cognitive_dissonance", "Detect conflicts between a new instruction and established LTM memories. force=True to skip discussion."),
302
548
  (handle_cognitive_resolve, "nexo_cognitive_resolve", "Resolve a cognitive dissonance: paradigm_shift, exception, or override."),
303
- (handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect user sentiment and get tone guidance. Also logs for tracking."),
549
+ (handle_cognitive_sentiment, "nexo_cognitive_sentiment", "Detect the user's sentiment and get tone guidance. Also logs for tracking."),
304
550
  (handle_cognitive_trust, "nexo_cognitive_trust", "View or adjust trust score (0-100). Without args: view. With event: adjust."),
551
+ (handle_cognitive_pin, "nexo_cognitive_pin", "Pin a memory — never decays, boosted +0.2 in search results."),
552
+ (handle_cognitive_snooze, "nexo_cognitive_snooze", "Snooze a memory — hidden from searches until a date, then auto-restores."),
553
+ (handle_cognitive_archive, "nexo_cognitive_archive", "Archive a memory — excluded from searches, can be restored."),
554
+ (handle_cognitive_restore, "nexo_cognitive_restore", "Restore a memory to active state (from pinned/snoozed/archived)."),
555
+ (handle_cognitive_quarantine_list, "nexo_cognitive_quarantine_list", "List quarantine queue items awaiting promotion to STM."),
556
+ (handle_cognitive_quarantine_promote, "nexo_cognitive_quarantine_promote", "Manually promote a quarantine item to STM."),
557
+ (handle_cognitive_quarantine_reject, "nexo_cognitive_quarantine_reject", "Manually reject a quarantine item."),
558
+ (handle_cognitive_quarantine_process, "nexo_cognitive_quarantine_process", "Run quarantine promotion cycle — evaluate pending items against policy."),
559
+ (handle_cognitive_trigger_create, "nexo_cognitive_trigger_create", "Create a prospective memory trigger — 'when X is mentioned, remind about Y'."),
560
+ (handle_cognitive_trigger_list, "nexo_cognitive_trigger_list", "List prospective triggers by status (armed/fired/all)."),
561
+ (handle_cognitive_trigger_check, "nexo_cognitive_trigger_check", "Check text against armed triggers. Returns fired triggers with actions."),
562
+ (handle_cognitive_trigger_delete, "nexo_cognitive_trigger_delete", "Delete a prospective trigger by ID."),
563
+ (handle_cognitive_trigger_rearm, "nexo_cognitive_trigger_rearm", "Re-arm a fired trigger so it can fire again."),
305
564
  ]
@@ -8,6 +8,7 @@ runs them in the correct order.
8
8
 
9
9
  Scheduled tasks (ordered by intended run time):
10
10
  03:00 — cognitive-decay (Ebbinghaus decay + STM→LTM promotion)
11
+ 03:00 — evolution (weekly, Sundays only)
11
12
  04:00 — sleep (session cleanup)
12
13
  07:00 — self-audit (health checks + weekly cognitive GC on Sundays)
13
14
  23:30 — postmortem (consolidation + sensory register)
@@ -23,14 +24,15 @@ import sys
23
24
  from datetime import datetime, timedelta
24
25
  from pathlib import Path
25
26
 
26
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
27
- LOG_DIR = NEXO_HOME / "logs"
27
+ HOME = Path.home()
28
+ LOG_DIR = HOME / "claude" / "logs"
28
29
  LOG_DIR.mkdir(parents=True, exist_ok=True)
29
30
  LOG_FILE = LOG_DIR / "catchup.log"
30
- STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
31
- SCRIPTS = NEXO_HOME / "src" / "scripts"
31
+ STATE_FILE = HOME / "claude" / "operations" / ".catchup-state.json"
32
32
 
33
- PYTHON = sys.executable
33
+ PYTHON_BREW = "/opt/homebrew/bin/python3"
34
+ PYTHON_SYS = "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3"
35
+ SCRIPTS = HOME / "claude" / "scripts"
34
36
 
35
37
 
36
38
  def log(msg: str):
@@ -93,7 +95,7 @@ def should_run(task_name: str, hour: int, minute: int, state: dict, weekday: int
93
95
  return last_run < last_scheduled
94
96
 
95
97
 
96
- def run_task(name: str, script: str, state: dict) -> bool:
98
+ def run_task(name: str, python: str, script: str, state: dict) -> bool:
97
99
  """Execute a task and update state."""
98
100
  script_path = str(SCRIPTS / script)
99
101
  if not Path(script_path).exists():
@@ -103,9 +105,9 @@ def run_task(name: str, script: str, state: dict) -> bool:
103
105
  log(f" RUNNING {name}: {script}")
104
106
  try:
105
107
  result = subprocess.run(
106
- [PYTHON, script_path],
108
+ [python, script_path],
107
109
  capture_output=True, text=True, timeout=300,
108
- env={**os.environ, "HOME": str(Path.home()), "NEXO_HOME": str(NEXO_HOME), "NEXO_CATCHUP": "1"}
110
+ env={**os.environ, "HOME": str(HOME), "NEXO_CATCHUP": "1"}
109
111
  )
110
112
  if result.returncode == 0:
111
113
  log(f" OK {name} (exit 0)")
@@ -129,20 +131,35 @@ def main():
129
131
  state = load_state()
130
132
 
131
133
  # Define tasks in execution order (matching their intended schedule order)
134
+ # Auto-update check FIRST
135
+ update_script = SCRIPTS / "nexo-auto-update.py"
136
+ if update_script.exists():
137
+ log("Checking for NEXO updates...")
138
+ try:
139
+ subprocess.run(
140
+ [PYTHON_BREW if os.path.exists(PYTHON_BREW) else PYTHON_SYS, str(update_script)],
141
+ capture_output=True, text=True, timeout=60,
142
+ env={**os.environ, "HOME": str(HOME), "NEXO_HOME": str(HOME / "claude" / "nexo-mcp")}
143
+ )
144
+ except Exception as e:
145
+ log(f" Update check failed: {e}")
146
+
132
147
  tasks = [
133
- # (name, hour, minute, script, weekday)
134
- ("cognitive-decay", 3, 0, "nexo-cognitive-decay.py", None),
135
- ("sleep", 4, 0, "nexo-sleep.py", None),
136
- ("self-audit", 7, 0, "nexo-daily-self-audit.py", None),
137
- ("postmortem", 23, 30, "nexo-postmortem-consolidator.py", None),
148
+ # (name, hour, minute, python, script, weekday)
149
+ ("cognitive-decay", 3, 0, PYTHON_BREW, "nexo-cognitive-decay.py", None),
150
+ ("evolution", 3, 0, PYTHON_SYS, "nexo-evolution-run.py", 6), # Sunday = 6
151
+ ("sleep", 4, 0, PYTHON_SYS, "nexo-sleep.py", None),
152
+ ("self-audit", 7, 0, PYTHON_SYS, "nexo-daily-self-audit.py", None),
153
+ ("github-monitor", 8, 0, PYTHON_BREW, "nexo-github-monitor.py", None),
154
+ ("postmortem", 23, 30, PYTHON_BREW, "nexo-postmortem-consolidator.py", None),
138
155
  ]
139
156
 
140
157
  ran = 0
141
158
  skipped = 0
142
- for name, hour, minute, script, weekday in tasks:
159
+ for name, hour, minute, python, script, weekday in tasks:
143
160
  if should_run(name, hour, minute, state, weekday):
144
161
  log(f" {name} — missed scheduled run, catching up...")
145
- if run_task(name, script, state):
162
+ if run_task(name, python, script, state):
146
163
  ran += 1
147
164
  else:
148
165
  skipped += 1
@@ -5,13 +5,11 @@ import json
5
5
  import sys
6
6
  from pathlib import Path
7
7
  from datetime import datetime
8
- import os
9
8
 
10
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
11
- sys.path.insert(0, str(NEXO_HOME / "src"))
9
+ sys.path.insert(0, str(Path.home() / "claude" / "nexo-mcp"))
12
10
  import cognitive
13
11
 
14
- STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
12
+ STATE_FILE = Path.home() / "claude" / "operations" / ".catchup-state.json"
15
13
 
16
14
 
17
15
  def update_catchup_state():