nexo-brain 2.4.0 → 2.5.1

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 (81) hide show
  1. package/README.md +80 -4
  2. package/bin/nexo-brain.js +238 -12
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +11 -3
  6. package/src/auto_update.py +193 -9
  7. package/src/cli.py +719 -0
  8. package/src/cognitive/_ingest.py +1 -1
  9. package/src/cognitive/_memory.py +4 -4
  10. package/src/crons/manifest.json +8 -0
  11. package/src/dashboard/app.py +700 -35
  12. package/src/dashboard/templates/adaptive.html +112 -218
  13. package/src/dashboard/templates/artifacts.html +133 -0
  14. package/src/dashboard/templates/backups.html +136 -0
  15. package/src/dashboard/templates/base.html +413 -0
  16. package/src/dashboard/templates/calendar.html +523 -654
  17. package/src/dashboard/templates/chat.html +356 -0
  18. package/src/dashboard/templates/claims.html +259 -0
  19. package/src/dashboard/templates/cortex.html +262 -0
  20. package/src/dashboard/templates/credentials.html +128 -0
  21. package/src/dashboard/templates/crons.html +370 -0
  22. package/src/dashboard/templates/dashboard.html +383 -578
  23. package/src/dashboard/templates/dreams.html +252 -0
  24. package/src/dashboard/templates/email.html +160 -0
  25. package/src/dashboard/templates/evolution.html +189 -0
  26. package/src/dashboard/templates/feed.html +249 -0
  27. package/src/dashboard/templates/followup_health.html +170 -0
  28. package/src/dashboard/templates/graph.html +191 -269
  29. package/src/dashboard/templates/guard.html +259 -0
  30. package/src/dashboard/templates/inbox.html +220 -346
  31. package/src/dashboard/templates/memory.html +317 -197
  32. package/src/dashboard/templates/operations.html +521 -698
  33. package/src/dashboard/templates/plugins.html +185 -0
  34. package/src/dashboard/templates/rules.html +246 -0
  35. package/src/dashboard/templates/sentiment.html +247 -0
  36. package/src/dashboard/templates/sessions.html +215 -182
  37. package/src/dashboard/templates/skills.html +329 -0
  38. package/src/dashboard/templates/somatic.html +68 -172
  39. package/src/dashboard/templates/triggers.html +133 -0
  40. package/src/dashboard/templates/trust.html +360 -0
  41. package/src/db/__init__.py +5 -0
  42. package/src/db/_schema.py +16 -1
  43. package/src/db/_sessions.py +22 -0
  44. package/src/db/_skills.py +980 -274
  45. package/src/doctor/__init__.py +1 -0
  46. package/src/doctor/formatters.py +52 -0
  47. package/src/doctor/models.py +44 -0
  48. package/src/doctor/orchestrator.py +42 -0
  49. package/src/doctor/providers/__init__.py +1 -0
  50. package/src/doctor/providers/boot.py +206 -0
  51. package/src/doctor/providers/deep.py +292 -0
  52. package/src/doctor/providers/runtime.py +686 -0
  53. package/src/evolution_cycle.py +86 -6
  54. package/src/hooks/post-compact.sh +5 -1
  55. package/src/hooks/pre-compact.sh +1 -1
  56. package/src/plugins/doctor.py +36 -0
  57. package/src/plugins/evolution.py +11 -3
  58. package/src/plugins/skills.py +135 -175
  59. package/src/requirements.txt +1 -0
  60. package/src/script_registry.py +322 -0
  61. package/src/scripts/deep-sleep/apply_findings.py +63 -48
  62. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  63. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  64. package/src/scripts/deep-sleep/synthesize.py +37 -1
  65. package/src/scripts/nexo-dashboard.sh +29 -0
  66. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  67. package/src/scripts/nexo-evolution-run.py +141 -54
  68. package/src/scripts/nexo-learning-housekeep.py +1 -1
  69. package/src/scripts/nexo-watchdog.sh +1 -1
  70. package/src/server.py +9 -5
  71. package/src/skills/run-runtime-doctor/guide.md +12 -0
  72. package/src/skills/run-runtime-doctor/script.py +21 -0
  73. package/src/skills/run-runtime-doctor/skill.json +25 -0
  74. package/src/skills_runtime.py +347 -0
  75. package/src/tools_menu.py +3 -2
  76. package/src/tools_sessions.py +126 -0
  77. package/src/user_context.py +46 -0
  78. package/templates/nexo_helper.py +45 -0
  79. package/templates/script-template.py +44 -0
  80. package/templates/skill-script-template.py +39 -0
  81. package/templates/skill-template.md +33 -0
@@ -0,0 +1,139 @@
1
+ #!/bin/bash
2
+ # ============================================================================
3
+ # NEXO Day Orchestrator — autonomous NEXO cycle every 15 min
4
+ # Schedule: keepAlive, self-enforced operating hours (default 8:00-23:00)
5
+ #
6
+ # This is NOT a Python script that simulates intelligence.
7
+ # This launches Claude Code as NEXO with full MCP access.
8
+ # NEXO thinks, acts, and reports — like any interactive session.
9
+ # ============================================================================
10
+ set -euo pipefail
11
+
12
+ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
13
+ LOG_DIR="$NEXO_HOME/logs"
14
+ mkdir -p "$LOG_DIR" "$NEXO_HOME/operations"
15
+
16
+ # --- Configuration ---
17
+ CYCLE_INTERVAL=900 # 15 minutes between cycles
18
+ CYCLE_TIMEOUT=600 # 10 min max per cycle
19
+ MAX_TURNS=30 # Claude max turns per cycle
20
+ HOUR_START=8
21
+ HOUR_END=23
22
+
23
+ # --- Find Claude CLI ---
24
+ find_claude() {
25
+ for candidate in \
26
+ "$(command -v claude 2>/dev/null)" \
27
+ "$HOME/.claude/local/claude" \
28
+ "/opt/homebrew/bin/claude" \
29
+ "/usr/local/bin/claude"; do
30
+ if [ -n "$candidate" ] && [ -x "$candidate" ]; then
31
+ echo "$candidate"
32
+ return 0
33
+ fi
34
+ done
35
+ return 1
36
+ }
37
+
38
+ CLAUDE=$(find_claude) || {
39
+ echo "$(date '+%Y-%m-%d %H:%M') ERROR: claude CLI not found" >&2
40
+ exit 1
41
+ }
42
+
43
+ # --- Prevent overlapping cycles ---
44
+ LOCKFILE="$NEXO_HOME/operations/.orchestrator.lock"
45
+ acquire_lock() {
46
+ if [ -f "$LOCKFILE" ]; then
47
+ local pid
48
+ pid=$(cat "$LOCKFILE" 2>/dev/null || echo "")
49
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
50
+ return 1 # Still running
51
+ fi
52
+ fi
53
+ echo $$ > "$LOCKFILE"
54
+ return 0
55
+ }
56
+ release_lock() { rm -f "$LOCKFILE"; }
57
+
58
+ # --- The orchestrator prompt ---
59
+ PROMPT='You are NEXO in autonomous orchestrator mode. The user is NOT present. You have 5 minutes max.
60
+
61
+ ABSOLUTE PRIORITY: act, do not list. If you can do something, do it. If you need the user, send email.
62
+
63
+ CHECKLIST (in this order):
64
+
65
+ 1. OVERDUE FOLLOWUPS: nexo_reminders(filter="due") + nexo_reminders(filter="followups")
66
+ - NEXO tasks (verify, check, monitor) → DO THEM NOW
67
+ - Tasks needing user decision → accumulate for email
68
+ - Completed ones → nexo_followup_complete
69
+
70
+ 2. EMAIL: nexo_email_inbox(unread_only=true, limit=10)
71
+ - Emails you can process → process them
72
+ - Important emails for user → accumulate for email
73
+
74
+ 3. INFRASTRUCTURE: nexo_doctor(tier="runtime")
75
+ - If degraded/critical → try to fix
76
+
77
+ 4. EMAIL TO USER (only if there is something to report):
78
+ - nexo_email_send with clean HTML summary
79
+ - Only what needs attention or decision
80
+ - Include what you ALREADY DID (not just pending items)
81
+ - If nothing relevant → DO NOT send email
82
+ - Max 1 email per cycle
83
+
84
+ 5. DIARY: nexo_session_diary_write with what you did
85
+
86
+ RULES:
87
+ - DO NOT ask permission. autonomy=full
88
+ - DO NOT send empty or "all ok" emails
89
+ - DO NOT list things without acting
90
+ - If a followup is executable → execute it before reporting
91
+ - Use nexo_heartbeat at start
92
+ - Clean close: diary + nexo_stop'
93
+
94
+ # --- Main loop ---
95
+ echo "$(date '+%Y-%m-%d %H:%M') NEXO Day Orchestrator starting (PID $$)"
96
+ echo " Claude: $CLAUDE"
97
+ echo " Cycle: every ${CYCLE_INTERVAL}s, ${HOUR_START}:00-${HOUR_END}:00"
98
+ echo " Timeout: ${CYCLE_TIMEOUT}s, max turns: $MAX_TURNS"
99
+
100
+ while true; do
101
+ HOUR=$(date +%H | sed 's/^0//')
102
+
103
+ # Outside operating hours — sleep and check again
104
+ if [ "$HOUR" -lt "$HOUR_START" ] || [ "$HOUR" -ge "$HOUR_END" ]; then
105
+ sleep 300 # Check every 5 min if we're back in hours
106
+ continue
107
+ fi
108
+
109
+ # Try to acquire lock
110
+ if ! acquire_lock; then
111
+ echo "$(date '+%Y-%m-%d %H:%M') Previous cycle still running. Skipping."
112
+ sleep "$CYCLE_INTERVAL"
113
+ continue
114
+ fi
115
+
116
+ TIMESTAMP=$(date '+%Y-%m-%d_%H%M')
117
+ LOGFILE="$LOG_DIR/orchestrator-$TIMESTAMP.log"
118
+ echo "$(date '+%Y-%m-%d %H:%M') Cycle starting..."
119
+
120
+ # Launch Claude Code as NEXO
121
+ set +e
122
+ timeout "$CYCLE_TIMEOUT" "$CLAUDE" \
123
+ --dangerously-skip-permissions \
124
+ -p "$PROMPT" \
125
+ --max-turns "$MAX_TURNS" \
126
+ >>"$LOGFILE" 2>&1
127
+ EXIT_CODE=$?
128
+ set -e
129
+
130
+ echo "$(date '+%Y-%m-%d %H:%M') Cycle finished (exit $EXIT_CODE)" | tee -a "$LOGFILE"
131
+
132
+ release_lock
133
+
134
+ # Clean old logs (keep 7 days)
135
+ find "$LOG_DIR" -name "orchestrator-*.log" -mtime +7 -delete 2>/dev/null || true
136
+
137
+ # Sleep until next cycle
138
+ sleep "$CYCLE_INTERVAL"
139
+ done
@@ -34,22 +34,6 @@ SANDBOX_DIR = CLAUDE_DIR / "sandbox" / "workspace"
34
34
  MAX_CONSECUTIVE_FAILURES = 3
35
35
  MAX_SNAPSHOTS = 8
36
36
 
37
- # ── Safe zones for AUTO execution ────────────────────────────────────────
38
- # "review" mode (owner): broader zones, but nothing executes without approval
39
- # "auto" mode (public users): restricted to user scripts and plugins ONLY
40
- AUTO_SAFE_PREFIXES = [
41
- str(CLAUDE_DIR / "scripts") + "/",
42
- str(CLAUDE_DIR / "brain") + "/",
43
- str(NEXO_CODE / "plugins") + "/",
44
- str(CLAUDE_DIR / "logs") + "/",
45
- str(CLAUDE_DIR / "coordination") + "/",
46
- ]
47
-
48
- # Public mode: only user-created scripts — NEVER core, cortex, or plugins
49
- AUTO_SAFE_PREFIXES_PUBLIC = [
50
- str(CLAUDE_DIR / "scripts") + "/",
51
- ]
52
-
53
37
  # ── Immutable files — NEVER touch (applies to ALL modes) ────────────────
54
38
  IMMUTABLE_FILES = {
55
39
  "db.py", "server.py", "plugin_loader.py", "nexo-watchdog.sh",
@@ -63,6 +47,52 @@ IMMUTABLE_FILES = {
63
47
  "tools_task_history.py", "tools_menu.py",
64
48
  }
65
49
 
50
+
51
+ def _repo_root() -> Path | None:
52
+ candidate = NEXO_CODE.parent
53
+ if (candidate / "package.json").exists():
54
+ return candidate
55
+ return None
56
+
57
+
58
+ def _public_safe_prefixes() -> list[str]:
59
+ return [
60
+ str(CLAUDE_DIR / "scripts") + "/",
61
+ str(CLAUDE_DIR / "plugins") + "/",
62
+ str(CLAUDE_DIR / "skills") + "/",
63
+ str(CLAUDE_DIR / "skills-runtime") + "/",
64
+ ]
65
+
66
+
67
+ def _managed_safe_prefixes() -> list[str]:
68
+ prefixes = [
69
+ str(CLAUDE_DIR / "scripts") + "/",
70
+ str(CLAUDE_DIR / "plugins") + "/",
71
+ str(CLAUDE_DIR / "brain") + "/",
72
+ str(CLAUDE_DIR / "coordination") + "/",
73
+ str(CLAUDE_DIR / "logs") + "/",
74
+ str(CLAUDE_DIR / "skills") + "/",
75
+ str(CLAUDE_DIR / "skills-core") + "/",
76
+ str(CLAUDE_DIR / "skills-runtime") + "/",
77
+ str(NEXO_CODE) + "/",
78
+ ]
79
+ repo_root = _repo_root()
80
+ if repo_root:
81
+ for rel in ("bin", "docs", "templates", "tests"):
82
+ prefixes.append(str(repo_root / rel) + "/")
83
+ return prefixes
84
+
85
+
86
+ def _normalize_mode(mode: str) -> str:
87
+ value = str(mode or "auto").strip().lower()
88
+ aliases = {
89
+ "owner": "managed",
90
+ "core": "managed",
91
+ "hybrid": "managed",
92
+ "manual": "review",
93
+ }
94
+ return aliases.get(value, value if value in {"auto", "review", "managed"} else "auto")
95
+
66
96
  # ── Claude CLI path ──────────────────────────────────────────────────────
67
97
  def _resolve_claude_cli() -> Path:
68
98
  """Find claude CLI: saved path > PATH > common locations."""
@@ -161,16 +191,18 @@ def call_claude_cli(prompt: str) -> str:
161
191
  # ── File safety validation ───────────────────────────────────────────────
162
192
  def is_safe_path(filepath: str, mode: str = "auto") -> bool:
163
193
  """Check if a file path is within safe zones and not immutable.
164
- mode='auto' (public): restricted to scripts/ and plugins/ only.
165
- mode='review' (owner): broader zones but nothing executes without approval anyway.
194
+ mode='auto' (public): restricted to personal automation surfaces.
195
+ mode='managed' (owner): broader repo/core surfaces with rollback.
196
+ mode='review': broader zones for proposal validation, but no execution.
166
197
  """
167
198
  expanded = str(Path(filepath).expanduser().resolve())
168
199
  filename = Path(expanded).name
200
+ mode = _normalize_mode(mode)
169
201
 
170
202
  if filename in IMMUTABLE_FILES:
171
203
  return False
172
204
 
173
- prefixes = AUTO_SAFE_PREFIXES if mode == "review" else AUTO_SAFE_PREFIXES_PUBLIC
205
+ prefixes = _managed_safe_prefixes() if mode in {"managed", "review"} else _public_safe_prefixes()
174
206
  for prefix in prefixes:
175
207
  resolved_prefix = str(Path(prefix).expanduser().resolve())
176
208
  if expanded.startswith(resolved_prefix):
@@ -217,13 +249,13 @@ def validate_syntax(filepath: str) -> tuple[bool, str]:
217
249
 
218
250
 
219
251
  # ── Apply a single change operation ──────────────────────────────────────
220
- def apply_change(change: dict) -> tuple[bool, str]:
252
+ def apply_change(change: dict, mode: str = "auto") -> tuple[bool, str]:
221
253
  """Apply a single file change operation. Returns (success, message)."""
222
254
  filepath = str(Path(change["file"]).expanduser())
223
255
  operation = change.get("operation", "")
224
256
  content = change.get("content", "")
225
257
 
226
- if not is_safe_path(filepath):
258
+ if not is_safe_path(filepath, mode=mode):
227
259
  return False, f"BLOCKED: {filepath} is outside safe zones or immutable"
228
260
 
229
261
  try:
@@ -268,7 +300,7 @@ def apply_change(change: dict) -> tuple[bool, str]:
268
300
 
269
301
 
270
302
  # ── Execute AUTO proposals ───────────────────────────────────────────────
271
- def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connection) -> dict:
303
+ def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connection, mode: str = "auto") -> dict:
272
304
  """Execute an AUTO proposal with snapshot/apply/validate/rollback."""
273
305
  changes = proposal.get("changes", [])
274
306
  if not changes:
@@ -277,7 +309,7 @@ def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connecti
277
309
  # Validate all paths first
278
310
  for change in changes:
279
311
  filepath = str(Path(change["file"]).expanduser())
280
- if not is_safe_path(filepath):
312
+ if not is_safe_path(filepath, mode=mode):
281
313
  return {"status": "blocked", "reason": f"Unsafe path: {filepath}"}
282
314
 
283
315
  # Collect files to snapshot (existing files only)
@@ -298,7 +330,7 @@ def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connecti
298
330
  all_results = []
299
331
  try:
300
332
  for change in changes:
301
- success, msg = apply_change(change)
333
+ success, msg = apply_change(change, mode=mode)
302
334
  all_results.append(msg)
303
335
  log(f" {msg}")
304
336
  if not success:
@@ -342,57 +374,102 @@ def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connecti
342
374
  log(f" Removed created file: {filepath}")
343
375
 
344
376
  return {
345
- "status": "failed",
377
+ "status": "rolled_back",
346
378
  "snapshot_ref": snapshot_ref,
347
379
  "files_changed": [],
348
380
  "test_result": f"ROLLBACK: {e}; " + "; ".join(all_results),
349
381
  }
350
382
 
351
383
 
352
- # ── Review followup for owner mode ──────────────────────────────────────
353
- def _create_review_followup(conn: sqlite3.Connection, cycle_num: int,
354
- items: list[dict], analysis: str):
355
- """Create a followup summarizing Evolution proposals for owner review."""
384
+ # ── Followups for managed/review modes ──────────────────────────────────
385
+ def _insert_followup(conn: sqlite3.Connection, followup_id: str, description: str,
386
+ verification: str, due_date: str | None = None):
387
+ now_epoch = datetime.now().timestamp()
388
+ conn.execute(
389
+ "INSERT OR REPLACE INTO followups (id, description, date, status, verification, created_at, updated_at) "
390
+ "VALUES (?, ?, ?, 'PENDING', ?, ?, ?)",
391
+ (followup_id, description, due_date, verification, now_epoch, now_epoch)
392
+ )
393
+ conn.commit()
394
+
395
+
396
+ def _create_cycle_followup(conn: sqlite3.Connection, cycle_num: int,
397
+ items: list[dict], analysis: str, mode: str):
398
+ """Create a followup summarizing pending proposals or owner review items."""
356
399
  tomorrow = (date.today() + timedelta(days=1)).isoformat()
357
400
  followup_id = f"NF-EVO-C{cycle_num}"
358
401
 
359
402
  public_items = [i for i in items if i.get("scope") == "public"]
360
403
  local_items = [i for i in items if i.get("scope") != "public"]
361
404
 
362
- lines = [f"Evolution Cycle #{cycle_num} {len(items)} proposals to review."]
405
+ title = "proposals to review" if mode == "review" else "items needing attention"
406
+ lines = [f"Evolution Cycle #{cycle_num} — {len(items)} {title}."]
363
407
  lines.append(f"Analysis: {analysis[:200]}")
364
408
  lines.append("")
365
409
 
366
410
  if public_items:
367
411
  lines.append(f"FOR EVERYONE ({len(public_items)}):")
368
412
  for i, item in enumerate(public_items, 1):
369
- lines.append(f" {i}. [{item['dimension']}] {item['action'][:120]}")
413
+ status = item.get("status", "proposed").upper()
414
+ lines.append(f" {i}. [{status}] [{item['dimension']}] {item['action'][:120]}")
370
415
  lines.append(f" Why: {item['reasoning'][:100]}")
416
+ if item.get("detail"):
417
+ lines.append(f" Detail: {item['detail'][:160]}")
371
418
  lines.append("")
372
419
 
373
420
  if local_items:
374
421
  lines.append(f"FOR YOU ONLY ({len(local_items)}):")
375
422
  for i, item in enumerate(local_items, 1):
376
- lines.append(f" {i}. [{item['dimension']}] {item['action'][:120]}")
423
+ status = item.get("status", "proposed").upper()
424
+ lines.append(f" {i}. [{status}] [{item['dimension']}] {item['action'][:120]}")
377
425
  lines.append(f" Why: {item['reasoning'][:100]}")
426
+ if item.get("detail"):
427
+ lines.append(f" Detail: {item['detail'][:160]}")
378
428
 
379
429
  description = "\n".join(lines)
380
430
 
381
431
  try:
382
- now_epoch = datetime.now().timestamp()
383
- conn.execute(
384
- "INSERT OR REPLACE INTO followups (id, description, date, status, verification, created_at, updated_at) "
385
- "VALUES (?, ?, ?, 'pending', ?, ?, ?)",
386
- (followup_id, description, tomorrow,
387
- f"SELECT * FROM evolution_log WHERE cycle_number={cycle_num}",
388
- now_epoch, now_epoch)
432
+ _insert_followup(
433
+ conn,
434
+ followup_id,
435
+ description,
436
+ f"SELECT * FROM evolution_log WHERE cycle_number={cycle_num}",
437
+ due_date=tomorrow,
389
438
  )
390
- conn.commit()
391
439
  log(f" Followup {followup_id} created for {tomorrow}")
392
440
  except Exception as e:
393
441
  log(f" WARN: Failed to create followup: {e}")
394
442
 
395
443
 
444
+ def _create_failure_followup(conn: sqlite3.Connection, cycle_num: int, log_id: int,
445
+ proposal: dict, result: dict):
446
+ """Create an incident-style followup for a failed or blocked AUTO proposal."""
447
+ followup_id = f"NF-EVO-L{log_id}"
448
+ lines = [
449
+ f"Evolution AUTO proposal failed in cycle #{cycle_num}.",
450
+ f"Action: {proposal.get('action', '')[:200]}",
451
+ f"Dimension: {proposal.get('dimension', 'other')}",
452
+ f"Status: {result.get('status', 'failed')}",
453
+ f"Reason: {(result.get('reason') or result.get('test_result') or 'unknown')[:400]}",
454
+ ]
455
+ snapshot_ref = result.get("snapshot_ref")
456
+ if snapshot_ref:
457
+ lines.append(f"Snapshot: {snapshot_ref}")
458
+ description = "\n".join(lines)
459
+
460
+ try:
461
+ _insert_followup(
462
+ conn,
463
+ followup_id,
464
+ description,
465
+ f"SELECT * FROM evolution_log WHERE id={log_id}",
466
+ due_date=(date.today() + timedelta(days=1)).isoformat(),
467
+ )
468
+ log(f" Failure followup {followup_id} created")
469
+ except Exception as e:
470
+ log(f" WARN: Failed to create failure followup: {e}")
471
+
472
+
396
473
  # ── Main run ─────────────────────────────────────────────────────────────
397
474
  def run():
398
475
  log("=" * 60)
@@ -481,14 +558,12 @@ def run():
481
558
  max_auto = max_auto_changes(objective.get("total_evolutions", 0))
482
559
  auto_count = 0
483
560
  auto_applied = 0
484
- evolution_mode = objective.get("evolution_mode", "auto") # "auto" (public) or "review" (owner)
561
+ evolution_mode = _normalize_mode(objective.get("evolution_mode", "auto"))
485
562
 
486
563
  conn = sqlite3.connect(str(NEXO_DB), timeout=10)
487
564
  conn.execute("PRAGMA busy_timeout=5000")
488
565
 
489
- # In "review" mode: log everything as pending_review, create followup
490
- # In "auto" mode: execute AUTO proposals, log PROPOSE as proposed
491
- review_items = []
566
+ followup_items = []
492
567
 
493
568
  for p in proposals:
494
569
  classification = p.get("classification", "propose")
@@ -498,30 +573,29 @@ def run():
498
573
  scope = p.get("scope", "local") # "public" or "local"
499
574
 
500
575
  if evolution_mode == "review":
501
- # Owner mode: nothing executes, everything queued for review
502
576
  log(f" QUEUED [{scope}]: {action[:80]}")
503
577
  conn.execute(
504
578
  "INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
505
579
  "reasoning, status) VALUES (?, ?, ?, ?, ?, ?)",
506
580
  (cycle_num, dimension, action, classification, reasoning, "pending_review")
507
581
  )
508
- review_items.append({
582
+ followup_items.append({
509
583
  "dimension": dimension,
510
584
  "action": action,
511
585
  "reasoning": reasoning,
512
586
  "scope": scope,
513
587
  "classification": classification,
588
+ "status": "pending_review",
514
589
  })
515
590
 
516
591
  elif classification == "auto" and auto_count < max_auto:
517
- # Public mode: execute AUTO proposals
518
592
  auto_count += 1
519
593
  log(f" AUTO #{auto_count}/{max_auto}: {action[:80]}")
520
594
 
521
- result = execute_auto_proposal(p, cycle_num, conn)
595
+ result = execute_auto_proposal(p, cycle_num, conn, mode=evolution_mode)
522
596
  status = result["status"]
523
597
 
524
- conn.execute(
598
+ cur = conn.execute(
525
599
  "INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
526
600
  "reasoning, status, files_changed, snapshot_ref, test_result) "
527
601
  "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
@@ -530,16 +604,20 @@ def run():
530
604
  result.get("snapshot_ref", ""),
531
605
  result.get("test_result", ""))
532
606
  )
607
+ log_id = cur.lastrowid
533
608
 
534
609
  if status == "applied":
535
610
  auto_applied += 1
536
611
  log(f" APPLIED successfully")
537
612
  elif status == "blocked":
538
- log(f" BLOCKED: {result.get('test_result', '')}")
613
+ detail = result.get("reason") or result.get("test_result", "")
614
+ log(f" BLOCKED: {detail[:100]}")
615
+ _create_failure_followup(conn, cycle_num, log_id, p, result)
539
616
  elif status == "skipped":
540
617
  log(f" SKIPPED: {result.get('reason', '')}")
541
618
  else:
542
- log(f" FAILED: {result.get('test_result', '')[:100]}")
619
+ log(f" ROLLED BACK: {result.get('test_result', '')[:100]}")
620
+ _create_failure_followup(conn, cycle_num, log_id, p, result)
543
621
 
544
622
  else:
545
623
  # PROPOSE or over auto limit
@@ -554,12 +632,20 @@ def run():
554
632
  "reasoning, status) VALUES (?, ?, ?, ?, ?, ?)",
555
633
  (cycle_num, dimension, action, classification, reasoning, "proposed")
556
634
  )
635
+ if evolution_mode in {"review", "managed"}:
636
+ followup_items.append({
637
+ "dimension": dimension,
638
+ "action": action,
639
+ "reasoning": reasoning,
640
+ "scope": scope,
641
+ "classification": classification,
642
+ "status": "proposed",
643
+ })
557
644
 
558
645
  conn.commit()
559
646
 
560
- # In review mode: create followup for owner
561
- if evolution_mode == "review" and review_items:
562
- _create_review_followup(conn, cycle_num, review_items, response.get("analysis", ""))
647
+ if evolution_mode in {"review", "managed"} and followup_items:
648
+ _create_cycle_followup(conn, cycle_num, followup_items, response.get("analysis", ""), evolution_mode)
563
649
 
564
650
  # Update metrics
565
651
  scores = response.get("dimension_scores", {})
@@ -590,6 +676,7 @@ def run():
590
676
  objective.setdefault("history", []).insert(0, {
591
677
  "cycle": cycle_num,
592
678
  "date": str(date.today()),
679
+ "mode": evolution_mode,
593
680
  "proposals": len(proposals),
594
681
  "auto_count": auto_count,
595
682
  "auto_applied": auto_applied,
@@ -70,7 +70,7 @@ def adjust_weights(conn):
70
70
  priority = l["priority"] or "medium"
71
71
 
72
72
  # Priority floor — critical learnings never drop below 0.5
73
- priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}[priority]
73
+ priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}.get(priority, 0.1)
74
74
 
75
75
  new_weight = old_weight
76
76
 
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # ============================================================================
3
3
  # NEXO Watchdog — Comprehensive health monitor for all NEXO services
4
- # Cron: */5 * * * * NEXO_HOME/scripts/nexo-watchdog.sh
4
+ # Schedule: every 30 minutes (interval_seconds: 1800)
5
5
  # ============================================================================
6
6
  # Monitors ALL LaunchAgents, cron jobs, and background processes.
7
7
  # Outputs: watchdog-status.json (machine), watchdog-report.txt (human),
package/src/server.py CHANGED
@@ -8,6 +8,7 @@ import sys
8
8
  from fastmcp import FastMCP
9
9
  from db import init_db, rebuild_fts_index, get_db, close_db, fts_add_dir, fts_remove_dir, fts_list_dirs
10
10
  from tools_sessions import handle_startup, handle_heartbeat, handle_status, handle_context_packet, handle_smart_startup_query
11
+ from user_context import get_context as _get_ctx
11
12
  from tools_coordination import (
12
13
  handle_track, handle_untrack, handle_files,
13
14
  handle_send, handle_ask, handle_answer, handle_check_answer,
@@ -154,12 +155,17 @@ def _server_init():
154
155
  mcp = FastMCP(
155
156
  name="nexo",
156
157
  instructions=(
157
- "NEXO — cognitive co-operator. Save important info from tool results before they clear.\n\n"
158
+ f"{_get_ctx().assistant_name} — cognitive co-operator. Save important info from tool results before they clear.\n\n"
159
+ "## CRITICAL — do these or you WILL get corrected\n"
160
+ "- **Guard (MANDATORY before ANY code edit):** `nexo_guard_check(files='...', area='...')` BEFORE editing code. "
161
+ "No exceptions. Blocking rules→resolve first. `nexo_track(sid=SID, paths=[...])` before shared files\n"
162
+ "- **Skills (MANDATORY before multi-step tasks):** `nexo_skill_match(task)` to find reusable procedures. "
163
+ "If match found, read it and follow the steps. After completion, `nexo_skill_result(id, success, context)` to record outcome.\n"
164
+ "- **Learnings (MANDATORY on corrections):** When you discover a bug, pattern, or get corrected→`nexo_learning_add` IMMEDIATELY. "
165
+ "Do NOT batch. Do NOT wait until end of session.\n\n"
158
166
  "## Rules\n"
159
167
  "- **Heartbeat:** `nexo_heartbeat(sid=SID, task='...', context_hint='...')` every user msg. "
160
168
  "React: DIARY REMINDER→write diary, VIBE:NEGATIVE→ultra-concise, AUTO-PRIME→read learnings\n"
161
- "- **Guard:** `nexo_guard_check(files='...', area='...')` BEFORE editing code. "
162
- "Blocking rules→resolve first. `nexo_track(sid=SID, paths=[...])` before shared files\n"
163
169
  "- **Followups:** NEXO tasks, execute silently. 'done'/'all set'→`nexo_followup_complete` NOW. "
164
170
  "Reminders=user's, alert when due\n"
165
171
  "- **Observe:** correction→learning. 'tomorrow'→followup. person→entity. open topic→followup 3d\n"
@@ -175,8 +181,6 @@ mcp = FastMCP(
175
181
  "write `nexo_session_diary_write(...)` with self_critique BEFORE responding. "
176
182
  "Detect intent, not keywords. If session closes without diary, auto_close handles it.\n"
177
183
  "- **Cortex:** `nexo_cortex_check` before budget/campaign/architecture changes\n"
178
- "- **Skills:** before multi-step tasks, `nexo_skill_match(task)` to find reusable procedures. "
179
- "If match found, read it and follow the steps. After completion, `nexo_skill_result(id, success, context)` to record outcome.\n"
180
184
  "- **Dissonance:** user contradicts memory→`nexo_cognitive_dissonance`. Frustrated→force=True\n"
181
185
  "- **Trust:** <40=paranoid verify twice, >80=fluid. Check: `nexo_cognitive_trust`"
182
186
  ),
@@ -0,0 +1,12 @@
1
+ # Run Runtime Doctor
2
+
3
+ Use this skill when you want a fast health snapshot of the running NEXO system.
4
+
5
+ ## Steps
6
+ 1. Run the runtime doctor for the requested tier.
7
+ 2. Review the degraded or critical checks first.
8
+ 3. If the report recommends deterministic fixes, decide whether to run them explicitly.
9
+
10
+ ## Gotchas
11
+ - A critical watchdog result reflects a real system issue, not just a stale skill.
12
+ - `all` is broader and slower than `runtime`.
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import subprocess
4
+ import sys
5
+
6
+
7
+ def main() -> int:
8
+ tier = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1] else "runtime"
9
+ nexo_code = os.environ.get("NEXO_CODE", "")
10
+ if not nexo_code:
11
+ print("NEXO_CODE not set", file=sys.stderr)
12
+ return 1
13
+
14
+ cli_py = os.path.join(nexo_code, "cli.py")
15
+ cmd = [sys.executable, cli_py, "doctor", "--tier", tier, "--json"]
16
+ result = subprocess.run(cmd, text=True)
17
+ return result.returncode
18
+
19
+
20
+ if __name__ == "__main__":
21
+ raise SystemExit(main())
@@ -0,0 +1,25 @@
1
+ {
2
+ "id": "SK-RUN-RUNTIME-DOCTOR",
3
+ "name": "Run Runtime Doctor",
4
+ "description": "Runs the NEXO runtime doctor and returns the current health report.",
5
+ "level": "published",
6
+ "mode": "execute",
7
+ "source_kind": "core",
8
+ "execution_level": "read-only",
9
+ "approval_required": false,
10
+ "tags": ["doctor", "diagnostics", "runtime"],
11
+ "trigger_patterns": ["run doctor", "check runtime health", "diagnose nexo"],
12
+ "params_schema": {
13
+ "tier": {
14
+ "type": "string",
15
+ "required": false,
16
+ "default": "runtime",
17
+ "enum": ["boot", "runtime", "deep", "all"]
18
+ }
19
+ },
20
+ "command_template": {
21
+ "argv": ["{{file_path}}", "{{tier}}"]
22
+ },
23
+ "executable_entry": "script.py",
24
+ "stable_after_uses": 10
25
+ }