nexo-brain 7.1.0 → 7.1.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.
Files changed (176) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -2
  3. package/bin/nexo-brain.js +198 -92
  4. package/package.json +1 -1
  5. package/src/agent_runner.py +10 -8
  6. package/src/auto_close_sessions.py +19 -2
  7. package/src/auto_update.py +305 -42
  8. package/src/autonomy_mandate.py +260 -0
  9. package/src/bootstrap_docs.py +22 -1
  10. package/src/cli.py +181 -1
  11. package/src/cli_email.py +104 -73
  12. package/src/client_sync.py +22 -1
  13. package/src/cognitive/_core.py +5 -3
  14. package/src/core_prompts.py +50 -0
  15. package/src/cron_recovery.py +81 -7
  16. package/src/crons/manifest.json +57 -0
  17. package/src/crons/sync.py +95 -26
  18. package/src/dashboard/app.py +59 -0
  19. package/src/dashboard/templates/base.html +2 -0
  20. package/src/dashboard/templates/feature-disabled.html +27 -0
  21. package/src/db/_email_accounts.py +67 -18
  22. package/src/db/_fts.py +5 -5
  23. package/src/db/_personal_scripts.py +1 -1
  24. package/src/db/_skills.py +3 -3
  25. package/src/doctor/providers/runtime.py +35 -20
  26. package/src/email_config.py +18 -9
  27. package/src/enforcement_classifier.py +3 -12
  28. package/src/evolution_cycle.py +37 -149
  29. package/src/guardian_telemetry.py +3 -2
  30. package/src/hook_guardrails.py +61 -0
  31. package/src/hooks/capture-tool-logs.sh +11 -3
  32. package/src/hooks/daily-briefing-check.sh +7 -2
  33. package/src/hooks/heartbeat-enforcement.py +14 -1
  34. package/src/hooks/heartbeat-posttool.sh +2 -0
  35. package/src/hooks/heartbeat-user-msg.sh +2 -0
  36. package/src/hooks/inbox-hook.sh +6 -2
  37. package/src/hooks/post-compact.sh +12 -4
  38. package/src/hooks/pre-compact.sh +12 -4
  39. package/src/migrate_embeddings.py +5 -3
  40. package/src/nexo_migrate.py +3 -1
  41. package/src/plugin_loader.py +14 -5
  42. package/src/plugins/adaptive_mode.py +4 -1
  43. package/src/plugins/backup.py +32 -20
  44. package/src/plugins/evolution.py +2 -0
  45. package/src/plugins/memory_export.py +6 -1
  46. package/src/plugins/personal_plugins.py +17 -7
  47. package/src/plugins/personal_scripts.py +64 -3
  48. package/src/presets/entities_universal.json +67 -4
  49. package/src/product_mode.py +201 -0
  50. package/src/r14_correction_learning.py +5 -20
  51. package/src/r15_project_context.py +4 -10
  52. package/src/r16_declared_done.py +3 -16
  53. package/src/r17_promise_debt.py +3 -16
  54. package/src/r18_followup_autocomplete.py +5 -7
  55. package/src/r19_project_grep.py +5 -8
  56. package/src/r20_constant_change.py +5 -15
  57. package/src/r21_legacy_path.py +5 -7
  58. package/src/r22_personal_script.py +4 -8
  59. package/src/r23_ssh_without_atlas.py +4 -11
  60. package/src/r23b_deploy_vhost.py +7 -6
  61. package/src/r23c_cwd_mismatch.py +7 -6
  62. package/src/r23d_chown_chmod_recursive.py +6 -6
  63. package/src/r23e_force_push_main.py +5 -6
  64. package/src/r23f_db_no_where.py +5 -6
  65. package/src/r23g_secrets_in_output.py +5 -5
  66. package/src/r23h_shebang_mismatch.py +6 -5
  67. package/src/r23i_auto_deploy_ignored.py +5 -6
  68. package/src/r23j_global_install.py +5 -6
  69. package/src/r23k_script_duplicates_skill.py +7 -6
  70. package/src/r23l_resource_collision.py +7 -6
  71. package/src/r23m_message_duplicate.py +6 -5
  72. package/src/r24_stale_memory.py +4 -9
  73. package/src/r25_nora_maria_read_only.py +5 -10
  74. package/src/r34_identity_coherence.py +6 -13
  75. package/src/r_catalog.py +3 -7
  76. package/src/resonance_map.py +13 -13
  77. package/src/runtime_power.py +29 -80
  78. package/src/script_registry.py +236 -6
  79. package/src/scripts/check-context.py +8 -25
  80. package/src/scripts/deep-sleep/extract.py +6 -10
  81. package/src/scripts/nexo-auto-update.py +27 -4
  82. package/src/scripts/nexo-catchup.py +9 -19
  83. package/src/scripts/nexo-cognitive-decay.py +26 -3
  84. package/src/scripts/nexo-daily-self-audit.py +50 -51
  85. package/src/scripts/nexo-email-migrate-config.py +30 -11
  86. package/src/scripts/nexo-email-monitor.py +97 -238
  87. package/src/scripts/nexo-followup-runner.py +70 -133
  88. package/src/scripts/nexo-hook-record.py +1 -1
  89. package/src/scripts/nexo-immune.py +6 -31
  90. package/src/scripts/nexo-impact-scorer.py +27 -4
  91. package/src/scripts/nexo-learning-housekeep.py +26 -3
  92. package/src/scripts/nexo-learning-validator.py +34 -32
  93. package/src/scripts/nexo-migrate.py +28 -12
  94. package/src/scripts/nexo-morning-agent.py +9 -23
  95. package/src/scripts/nexo-outcome-checker.py +27 -4
  96. package/src/scripts/nexo-postmortem-consolidator.py +30 -62
  97. package/src/scripts/nexo-pre-commit.py +28 -0
  98. package/src/scripts/nexo-proactive-dashboard.py +27 -0
  99. package/src/scripts/nexo-reflection.py +33 -3
  100. package/src/scripts/nexo-runtime-preflight.py +27 -2
  101. package/src/scripts/nexo-send-reply.py +10 -8
  102. package/src/scripts/nexo-sleep.py +11 -25
  103. package/src/scripts/nexo-synthesis.py +7 -40
  104. package/src/scripts/nexo-watchdog-smoke.py +30 -1
  105. package/src/scripts/nexo-watchdog.sh +23 -17
  106. package/src/scripts/phase_guardian_analysis.py +27 -4
  107. package/src/server.py +14 -3
  108. package/src/storage_router.py +8 -6
  109. package/src/tools_drive.py +5 -13
  110. package/src/tools_guardian.py +3 -4
  111. package/src/tools_menu.py +2 -2
  112. package/src/tools_reminders_crud.py +17 -0
  113. package/src/tools_sessions.py +1 -4
  114. package/src/user_context.py +3 -6
  115. package/src/user_data_portability.py +31 -23
  116. package/templates/CLAUDE.md.template +11 -3
  117. package/templates/CODEX.AGENTS.md.template +11 -3
  118. package/templates/core-prompts/catchup-assessment.md +19 -0
  119. package/templates/core-prompts/check-context.md +24 -0
  120. package/templates/core-prompts/daily-self-audit.md +42 -0
  121. package/templates/core-prompts/daily-synthesis.md +40 -0
  122. package/templates/core-prompts/deep-sleep-extract-json-output.md +8 -0
  123. package/templates/core-prompts/drive-signal-classifier-system.md +4 -0
  124. package/templates/core-prompts/drive-signal-classifier-user.md +6 -0
  125. package/templates/core-prompts/email-monitor.md +202 -0
  126. package/templates/core-prompts/enforcement-classifier-retry.md +1 -0
  127. package/templates/core-prompts/enforcement-classifier-strict.md +1 -0
  128. package/templates/core-prompts/evolution-public-contribution.md +32 -0
  129. package/templates/core-prompts/evolution-public-pr-review.md +38 -0
  130. package/templates/core-prompts/evolution-weekly.md +71 -0
  131. package/templates/core-prompts/followup-runner-operator-attention-context.md +4 -0
  132. package/templates/core-prompts/followup-runner-operator-attention-question.md +1 -0
  133. package/templates/core-prompts/followup-runner.md +74 -0
  134. package/templates/core-prompts/immune-triage.md +31 -0
  135. package/templates/core-prompts/interactive-startup.md +1 -0
  136. package/templates/core-prompts/json-object-only.md +1 -0
  137. package/templates/core-prompts/learning-validator.md +25 -0
  138. package/templates/core-prompts/morning-agent-json-output.md +1 -0
  139. package/templates/core-prompts/morning-agent.md +23 -0
  140. package/templates/core-prompts/postmortem-consolidator.md +60 -0
  141. package/templates/core-prompts/r-catalog.md +1 -0
  142. package/templates/core-prompts/r14-correction-learning-injection.md +1 -0
  143. package/templates/core-prompts/r14-correction-learning-question.md +1 -0
  144. package/templates/core-prompts/r15-project-context-injection.md +1 -0
  145. package/templates/core-prompts/r16-declared-done-injection.md +1 -0
  146. package/templates/core-prompts/r16-declared-done-question.md +1 -0
  147. package/templates/core-prompts/r17-promise-debt-injection.md +1 -0
  148. package/templates/core-prompts/r17-promise-debt-question.md +1 -0
  149. package/templates/core-prompts/r18-followup-autocomplete-injection.md +3 -0
  150. package/templates/core-prompts/r19-project-grep-injection.md +1 -0
  151. package/templates/core-prompts/r20-constant-change-injection.md +1 -0
  152. package/templates/core-prompts/r20-constant-change-question.md +1 -0
  153. package/templates/core-prompts/r21-legacy-path-injection.md +1 -0
  154. package/templates/core-prompts/r22-personal-script-injection.md +1 -0
  155. package/templates/core-prompts/r23-ssh-without-atlas-injection.md +1 -0
  156. package/templates/core-prompts/r23b-deploy-vhost-injection.md +1 -0
  157. package/templates/core-prompts/r23c-cwd-mismatch-injection.md +1 -0
  158. package/templates/core-prompts/r23d-chown-chmod-recursive-injection.md +1 -0
  159. package/templates/core-prompts/r23e-force-push-main-injection.md +1 -0
  160. package/templates/core-prompts/r23f-db-no-where-injection.md +1 -0
  161. package/templates/core-prompts/r23g-secrets-in-output-injection.md +1 -0
  162. package/templates/core-prompts/r23h-shebang-mismatch-injection.md +1 -0
  163. package/templates/core-prompts/r23i-auto-deploy-ignored-injection.md +1 -0
  164. package/templates/core-prompts/r23j-global-install-injection.md +1 -0
  165. package/templates/core-prompts/r23k-script-duplicates-skill-injection.md +1 -0
  166. package/templates/core-prompts/r23l-resource-collision-injection.md +1 -0
  167. package/templates/core-prompts/r23m-message-duplicate-injection.md +1 -0
  168. package/templates/core-prompts/r24-stale-memory-injection.md +1 -0
  169. package/templates/core-prompts/r25-read-only-host-injection.md +1 -0
  170. package/templates/core-prompts/r34-identity-coherence-probe.md +1 -0
  171. package/templates/core-prompts/r34-identity-coherence-question.md +1 -0
  172. package/templates/core-prompts/sleep.md +25 -0
  173. package/templates/email-template.md +55 -0
  174. package/templates/nexo_helper.py +31 -13
  175. package/templates/plugin-template.py +3 -3
  176. package/templates/skill-template.md +2 -1
@@ -6,6 +6,8 @@ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
6
6
  HELPER=""
7
7
  if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/hooks/heartbeat-enforcement.py" ]; then
8
8
  HELPER="${NEXO_CODE%/}/hooks/heartbeat-enforcement.py"
9
+ elif [ -f "$NEXO_HOME/core/hooks/heartbeat-enforcement.py" ]; then
10
+ HELPER="$NEXO_HOME/core/hooks/heartbeat-enforcement.py"
9
11
  elif [ -f "$NEXO_HOME/hooks/heartbeat-enforcement.py" ]; then
10
12
  HELPER="$NEXO_HOME/hooks/heartbeat-enforcement.py"
11
13
  fi
@@ -28,8 +28,12 @@ echo "$NOW" > "$DEBOUNCE_FILE"
28
28
 
29
29
  # 4. Find NEXO SID mapped to this Claude session_id
30
30
  NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
31
- DB="$NEXO_HOME/data/nexo.db"
32
- mkdir -p "$NEXO_HOME/data"
31
+ DATA_DIR="$NEXO_HOME/runtime/data"
32
+ if [ ! -d "$DATA_DIR" ] && [ -d "$NEXO_HOME/data" ]; then
33
+ DATA_DIR="$NEXO_HOME/data"
34
+ fi
35
+ DB="$DATA_DIR/nexo.db"
36
+ mkdir -p "$DATA_DIR"
33
37
  [ -f "$DB" ] || exit 0
34
38
 
35
39
  NEXO_SID=$(sqlite3 "$DB" "SELECT sid FROM sessions WHERE (external_session_id = '${CLAUDE_SID}' OR claude_session_id = '${CLAUDE_SID}') AND last_update_epoch > (strftime('%s','now') - 900) ORDER BY last_update_epoch DESC LIMIT 1;" 2>/dev/null)
@@ -5,10 +5,18 @@
5
5
  set -uo pipefail
6
6
 
7
7
  NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
8
- NEXO_DB="$NEXO_HOME/data/nexo.db"
9
- mkdir -p "$NEXO_HOME/data"
8
+ DATA_DIR="$NEXO_HOME/runtime/data"
9
+ if [ ! -d "$DATA_DIR" ] && [ -d "$NEXO_HOME/data" ]; then
10
+ DATA_DIR="$NEXO_HOME/data"
11
+ fi
12
+ OPERATIONS_DIR="$NEXO_HOME/runtime/operations"
13
+ if [ ! -d "$OPERATIONS_DIR" ] && [ -d "$NEXO_HOME/operations" ]; then
14
+ OPERATIONS_DIR="$NEXO_HOME/operations"
15
+ fi
16
+ NEXO_DB="$DATA_DIR/nexo.db"
17
+ mkdir -p "$DATA_DIR" "$OPERATIONS_DIR"
10
18
  TODAY=$(date +%Y-%m-%d)
11
- LOG_FILE="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
19
+ LOG_FILE="$OPERATIONS_DIR/tool-logs/${TODAY}.jsonl"
12
20
  LOG_LINES=0
13
21
  if [ -f "$LOG_FILE" ]; then
14
22
  LOG_LINES=$(wc -l < "$LOG_FILE" | tr -d ' ')
@@ -120,7 +128,7 @@ if [ -f "$NEXO_DB" ]; then
120
128
  BLOCK="$BLOCK\n**Session tasks so far:** $TASKS_SEEN"
121
129
  fi
122
130
 
123
- BLOCK="$BLOCK\n**Tool logs:** ${NEXO_HOME}/operations/tool-logs/${TODAY}.jsonl ($LOG_LINES entries)"
131
+ BLOCK="$BLOCK\n**Tool logs:** ${OPERATIONS_DIR}/tool-logs/${TODAY}.jsonl ($LOG_LINES entries)"
124
132
  BLOCK="$BLOCK\n\n**POST-COMPACTION INSTRUCTIONS:**"
125
133
  BLOCK="$BLOCK\n1. Call nexo_heartbeat with the SID above to reconnect with the session"
126
134
  BLOCK="$BLOCK\n2. If you need specific lost data, query tool logs with jq"
@@ -7,10 +7,18 @@ set -uo pipefail
7
7
 
8
8
  HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
9
9
  NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
10
- NEXO_DB="$NEXO_HOME/data/nexo.db"
11
- mkdir -p "$NEXO_HOME/data"
10
+ DATA_DIR="$NEXO_HOME/runtime/data"
11
+ if [ ! -d "$DATA_DIR" ] && [ -d "$NEXO_HOME/data" ]; then
12
+ DATA_DIR="$NEXO_HOME/data"
13
+ fi
14
+ OPERATIONS_DIR="$NEXO_HOME/runtime/operations"
15
+ if [ ! -d "$OPERATIONS_DIR" ] && [ -d "$NEXO_HOME/operations" ]; then
16
+ OPERATIONS_DIR="$NEXO_HOME/operations"
17
+ fi
18
+ NEXO_DB="$DATA_DIR/nexo.db"
19
+ mkdir -p "$DATA_DIR" "$OPERATIONS_DIR"
12
20
  TODAY=$(date +%Y-%m-%d)
13
- LOG_FILE="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
21
+ LOG_FILE="$OPERATIONS_DIR/tool-logs/${TODAY}.jsonl"
14
22
  LOG_LINES=0
15
23
  if [ -f "$LOG_FILE" ]; then
16
24
  LOG_LINES=$(wc -l < "$LOG_FILE" | tr -d ' ')
@@ -164,6 +172,6 @@ fi
164
172
 
165
173
  cat << HOOKEOF
166
174
  {
167
- "systemMessage": "CONTEXT IS ABOUT TO BE COMPRESSED.\n\nOBLIGATORY ACTIONS BEFORE COMPACTION:\n1. Save critical state via MCP: nexo_checkpoint_save with current task, active files, decisions, errors, next step, and reasoning thread.\n2. If there is work in progress without a commit, save data via nexo_entity_create, nexo_preference_set, nexo_learning_add, nexo_followup_create.\n3. PERSISTENT TOOL LOGS: ${NEXO_HOME}/operations/tool-logs/${TODAY}.jsonl has ${LOG_LINES} entries.\n4. After compaction, the PostCompact hook will re-inject a Core Memory Block with the checkpoint.\n5. MCP tools (nexo_*) preserve all state — use them to recover context.\n6. EMERGENCY DIARY: An automatic diary was written by the pre-compact hook. The LLM can still write a better one via nexo_session_diary_write."
175
+ "systemMessage": "CONTEXT IS ABOUT TO BE COMPRESSED.\n\nOBLIGATORY ACTIONS BEFORE COMPACTION:\n1. Save critical state via MCP: nexo_checkpoint_save with current task, active files, decisions, errors, next step, and reasoning thread.\n2. If there is work in progress without a commit, save data via nexo_entity_create, nexo_preference_set, nexo_learning_add, nexo_followup_create.\n3. PERSISTENT TOOL LOGS: ${OPERATIONS_DIR}/tool-logs/${TODAY}.jsonl has ${LOG_LINES} entries.\n4. After compaction, the PostCompact hook will re-inject a Core Memory Block with the checkpoint.\n5. MCP tools (nexo_*) preserve all state — use them to recover context.\n6. EMERGENCY DIARY: An automatic diary was written by the pre-compact hook. The LLM can still write a better one via nexo_session_diary_write."
168
176
  }
169
177
  HOOKEOF
@@ -15,10 +15,12 @@ import sys
15
15
  import time
16
16
  import numpy as np
17
17
 
18
+ import paths
19
+
18
20
  NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
19
- _data_dir = os.path.join(NEXO_HOME, "data")
20
- os.makedirs(_data_dir, exist_ok=True)
21
- DB_PATH = os.path.join(_data_dir, "cognitive.db")
21
+ _cognitive_dir = paths.cognitive_dir()
22
+ _cognitive_dir.mkdir(parents=True, exist_ok=True)
23
+ DB_PATH = str(_cognitive_dir / "cognitive.db")
22
24
  BACKUP_PATH = DB_PATH + ".bak-384dims-pre-upgrade"
23
25
 
24
26
  MODELS = {
@@ -30,6 +30,8 @@ import time
30
30
  from pathlib import Path
31
31
  from typing import Callable
32
32
 
33
+ import paths
34
+
33
35
 
34
36
  def _nexo_home() -> Path:
35
37
  env = os.environ.get("NEXO_HOME")
@@ -42,7 +44,7 @@ def _db_path() -> Path:
42
44
  env = os.environ.get("NEXO_DB_PATH")
43
45
  if env:
44
46
  return Path(env)
45
- return _nexo_home() / "data" / "nexo.db"
47
+ return paths.resolve_db_path()
46
48
 
47
49
 
48
50
  def _structure_version_path() -> Path:
@@ -8,6 +8,7 @@ import signal
8
8
  import sys
9
9
  import time
10
10
 
11
+ import paths
11
12
  from db import get_db
12
13
  from fastmcp.tools import Tool
13
14
 
@@ -25,9 +26,9 @@ except ModuleNotFoundError as exc:
25
26
  SERVER_DIR = os.path.dirname(os.path.abspath(__file__))
26
27
  PLUGINS_DIR = os.path.join(SERVER_DIR, "plugins")
27
28
 
28
- # Personal plugins directory: NEXO_HOME/plugins/ (env var, defaults to ~/.nexo/)
29
+ # Personal plugins directory: NEXO_HOME/personal/plugins/ (env var, defaults to ~/.nexo/)
29
30
  NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
30
- PERSONAL_PLUGINS_DIR = os.path.join(NEXO_HOME, "plugins")
31
+ PERSONAL_PLUGINS_DIR = str(paths.personal_plugins_dir())
31
32
 
32
33
  PLUGIN_LOAD_TIMEOUT = 10 # seconds per plugin
33
34
 
@@ -141,14 +142,22 @@ def load_all_plugins(mcp) -> int:
141
142
  continue
142
143
  plugin_map[f] = (PLUGINS_DIR, "repo")
143
144
 
144
- # 2. Personal plugins (override if same filename)
145
+ # 2. Personal plugins. Never let a personal file shadow a packaged core
146
+ # plugin at startup: creation already rejects that collision, and keeping
147
+ # the repo plugin canonical avoids hybrid installs with two sources of truth.
145
148
  if os.path.isdir(PERSONAL_PLUGINS_DIR):
146
149
  for f in sorted(os.listdir(PERSONAL_PLUGINS_DIR)):
147
150
  if f.endswith(".py") and f != "__init__.py":
148
151
  if is_duplicate_artifact_name(os.path.join(PERSONAL_PLUGINS_DIR, f)):
149
152
  continue
150
- source = "personal (override)" if f in plugin_map else "personal"
151
- plugin_map[f] = (PERSONAL_PLUGINS_DIR, source)
153
+ if f in plugin_map:
154
+ print(
155
+ f"[PLUGIN SHADOW SKIP] {f}: personal plugin collides with packaged core filename; "
156
+ "keeping core plugin canonical",
157
+ file=sys.stderr,
158
+ )
159
+ continue
160
+ plugin_map[f] = (PERSONAL_PLUGINS_DIR, "personal")
152
161
 
153
162
  # Load all in sorted order
154
163
  for f in sorted(plugin_map):
@@ -28,10 +28,13 @@ import time
28
28
  import math
29
29
  import subprocess
30
30
  from datetime import datetime, timedelta
31
+ from pathlib import Path
32
+
33
+ import paths
31
34
  from db import get_db
32
35
 
33
36
  NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
34
- ADAPTIVE_STATE_FILE = os.path.join(NEXO_HOME, "brain", "adaptive_state.json")
37
+ ADAPTIVE_STATE_FILE = str(paths.brain_dir() / "adaptive_state.json")
35
38
 
36
39
  # Mode definitions
37
40
  MODES = {
@@ -14,12 +14,12 @@ import shutil
14
14
  import sqlite3
15
15
  import threading
16
16
  import time
17
+ from pathlib import Path
17
18
 
19
+ import paths
18
20
  from db import get_db
19
21
 
20
22
  NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
21
- DB_PATH = os.path.join(NEXO_HOME, "data", "nexo.db")
22
- BACKUP_DIR = os.path.join(NEXO_HOME, "backups")
23
23
 
24
24
  RETENTION_DAYS = 7
25
25
 
@@ -66,6 +66,14 @@ def _reset_rate_limit_state_for_tests() -> None:
66
66
  _last_call_ts[key] = 0.0
67
67
 
68
68
 
69
+ def _db_path() -> Path:
70
+ return paths.db_path()
71
+
72
+
73
+ def _backup_dir() -> Path:
74
+ return paths.backups_dir()
75
+
76
+
69
77
  def handle_backup_now() -> str:
70
78
  """Create an immediate backup of the NEXO database.
71
79
 
@@ -75,14 +83,15 @@ def handle_backup_now() -> str:
75
83
  if err is not None:
76
84
  return err
77
85
 
78
- os.makedirs(BACKUP_DIR, exist_ok=True)
86
+ backup_dir = _backup_dir()
87
+ backup_dir.mkdir(parents=True, exist_ok=True)
79
88
  timestamp = time.strftime("%Y-%m-%d-%H%M")
80
- dest = os.path.join(BACKUP_DIR, f"nexo-{timestamp}.db")
89
+ dest = backup_dir / f"nexo-{timestamp}.db"
81
90
 
82
91
  # Use SQLite backup API for consistency
83
- src_conn = sqlite3.connect(DB_PATH)
92
+ src_conn = sqlite3.connect(str(_db_path()))
84
93
  try:
85
- dst_conn = sqlite3.connect(dest)
94
+ dst_conn = sqlite3.connect(str(dest))
86
95
  try:
87
96
  src_conn.backup(dst_conn)
88
97
  finally:
@@ -90,16 +99,17 @@ def handle_backup_now() -> str:
90
99
  finally:
91
100
  src_conn.close()
92
101
 
93
- size_kb = os.path.getsize(dest) / 1024
102
+ size_kb = dest.stat().st_size / 1024
94
103
  _cleanup_old()
95
- return f"Backup created: {os.path.basename(dest)} ({size_kb:.0f} KB)"
104
+ return f"Backup created: {dest.name} ({size_kb:.0f} KB)"
96
105
 
97
106
 
98
107
  def handle_backup_list() -> str:
99
108
  """List available backups with dates and sizes."""
100
- if not os.path.isdir(BACKUP_DIR):
109
+ backup_dir = _backup_dir()
110
+ if not backup_dir.is_dir():
101
111
  return "No backups."
102
- files = sorted(glob.glob(os.path.join(BACKUP_DIR, "nexo-*.db")), reverse=True)
112
+ files = sorted(glob.glob(str(backup_dir / "nexo-*.db")), reverse=True)
103
113
  if not files:
104
114
  return "No backups."
105
115
  lines = [f"BACKUPS ({len(files)}):"]
@@ -127,15 +137,16 @@ def handle_backup_restore(filename: str) -> str:
127
137
  if err is not None:
128
138
  return err
129
139
 
130
- src = os.path.join(BACKUP_DIR, filename)
131
- if not os.path.isfile(src):
140
+ backup_dir = _backup_dir()
141
+ src = backup_dir / filename
142
+ if not src.is_file():
132
143
  return f"Backup not found: {filename}"
133
144
 
134
145
  # Create safety backup first
135
- safety = os.path.join(BACKUP_DIR, f"nexo-pre-restore-{time.strftime('%Y%m%d%H%M%S')}.db")
136
- src_conn = sqlite3.connect(DB_PATH)
146
+ safety = backup_dir / f"nexo-pre-restore-{time.strftime('%Y%m%d%H%M%S')}.db"
147
+ src_conn = sqlite3.connect(str(_db_path()))
137
148
  try:
138
- dst_conn = sqlite3.connect(safety)
149
+ dst_conn = sqlite3.connect(str(safety))
139
150
  try:
140
151
  src_conn.backup(dst_conn)
141
152
  finally:
@@ -144,9 +155,9 @@ def handle_backup_restore(filename: str) -> str:
144
155
  src_conn.close()
145
156
 
146
157
  # Restore
147
- restore_conn = sqlite3.connect(src)
158
+ restore_conn = sqlite3.connect(str(src))
148
159
  try:
149
- target_conn = sqlite3.connect(DB_PATH)
160
+ target_conn = sqlite3.connect(str(_db_path()))
150
161
  try:
151
162
  restore_conn.backup(target_conn)
152
163
  finally:
@@ -163,7 +174,7 @@ def handle_backup_restore(filename: str) -> str:
163
174
  pass
164
175
  db._shared_conn = None
165
176
 
166
- return f"DB restaurada desde {filename}. Safety backup: {os.path.basename(safety)}"
177
+ return f"DB restaurada desde {filename}. Safety backup: {safety.name}"
167
178
 
168
179
 
169
180
  def _cleanup_old():
@@ -173,12 +184,13 @@ def _cleanup_old():
173
184
  `nexo-pre-restore-*.db` safety snapshots created by handle_backup_restore.
174
185
  Failures are swallowed — housekeeping must never interrupt the caller.
175
186
  """
176
- if not os.path.isdir(BACKUP_DIR):
187
+ backup_dir = _backup_dir()
188
+ if not backup_dir.is_dir():
177
189
  return
178
190
  cutoff = time.time() - (RETENTION_DAYS * 86400)
179
191
  # glob `nexo-*.db` matches both the hourly pattern and pre-restore
180
192
  # snapshots, so a single loop prunes both with a single pass.
181
- for f in glob.glob(os.path.join(BACKUP_DIR, "nexo-*.db")):
193
+ for f in glob.glob(str(backup_dir / "nexo-*.db")):
182
194
  try:
183
195
  if os.path.getmtime(f) < cutoff:
184
196
  os.remove(f)
@@ -42,6 +42,8 @@ def handle_evolution_status() -> str:
42
42
 
43
43
  from user_context import get_context
44
44
  lines = [f"{get_context().assistant_name} EVOLUTION STATUS:"]
45
+ if objective and objective.get("evolution_enabled") is False:
46
+ lines.append(f" Disabled: {objective.get('disabled_reason', 'unknown')}")
45
47
  has_output = False
46
48
  for key, label in CANONICAL_DIMENSIONS.items():
47
49
  m = metrics.get(key)
@@ -11,6 +11,7 @@ import cognitive
11
11
  import claim_graph
12
12
  import compaction_memory
13
13
  import media_memory
14
+ import paths
14
15
  import user_state_model
15
16
  from db import get_db
16
17
  from memory_backends import get_backend, list_backends
@@ -69,7 +70,11 @@ def handle_memory_export(format: str = "markdown", output_dir: str = "") -> str:
69
70
  return "ERROR: only markdown export is supported for now."
70
71
 
71
72
  stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
72
- root = Path(output_dir).expanduser() if output_dir.strip() else (_nexo_home() / "exports" / "memory" / stamp)
73
+ root = (
74
+ Path(output_dir).expanduser()
75
+ if output_dir.strip()
76
+ else (paths.exports_dir() / "memory" / stamp)
77
+ )
73
78
  root.mkdir(parents=True, exist_ok=True)
74
79
 
75
80
  conn = get_db()
@@ -1,4 +1,4 @@
1
- """NEXO Personal Plugins — scaffold persistent MCP tools in NEXO_HOME/plugins."""
1
+ """NEXO Personal Plugins — scaffold persistent MCP tools in NEXO_HOME/personal/plugins."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -16,11 +16,15 @@ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent
16
16
 
17
17
 
18
18
  def _plugins_dir() -> Path:
19
- path = paths.core_plugins_dir()
19
+ path = paths.personal_plugins_dir()
20
20
  path.mkdir(parents=True, exist_ok=True)
21
21
  return path
22
22
 
23
23
 
24
+ def _core_plugin_path(filename: str) -> Path:
25
+ return paths.core_plugins_dir() / filename
26
+
27
+
24
28
  def _safe_slug(value: str) -> str:
25
29
  chars: list[str] = []
26
30
  for ch in str(value or "").lower():
@@ -68,11 +72,11 @@ def _render_plugin_template(*, plugin_stem: str, tool_name: str, description: st
68
72
  content = content.replace("handle_example_tool", handler_name)
69
73
  content = content.replace("nexo_example_tool", tool_name)
70
74
  content = content.replace(
71
- "Personal plugin scaffold created. Edit this handler in NEXO_HOME/plugins.",
75
+ "Personal plugin scaffold created. Edit this handler in NEXO_HOME/personal/plugins.",
72
76
  description or f"Personal plugin scaffold for {plugin_stem}.",
73
77
  )
74
78
  content = content.replace(
75
- "Example personal MCP tool scaffold. Edit it in NEXO_HOME/plugins.",
79
+ "Example personal MCP tool scaffold. Edit it in NEXO_HOME/personal/plugins.",
76
80
  description or f"Personal MCP tool scaffold for {plugin_stem}.",
77
81
  )
78
82
  return content
@@ -86,15 +90,21 @@ def handle_personal_plugin_create(
86
90
  script_runtime: str = "python",
87
91
  force: bool = False,
88
92
  ) -> str:
89
- """Create a personal MCP plugin scaffold in NEXO_HOME/plugins.
93
+ """Create a personal MCP plugin scaffold in NEXO_HOME/personal/plugins.
90
94
 
91
- Optionally also creates a companion script in NEXO_HOME/scripts.
95
+ Optionally also creates a companion script in NEXO_HOME/personal/scripts.
92
96
  """
93
97
  init_db()
94
98
  plugin_stem = _safe_slug(name)
95
99
  filename = f"{plugin_stem}.py"
96
100
  tool_name = (tool_name or f"nexo_{plugin_stem.replace('-', '_')}").strip()
97
101
  plugin_path = _plugins_dir() / filename
102
+ core_plugin_path = _core_plugin_path(filename)
103
+ if core_plugin_path.exists() and not force:
104
+ return json.dumps({
105
+ "ok": False,
106
+ "error": f"Personal plugin name collides with a core plugin identity: {filename}",
107
+ }, ensure_ascii=False)
98
108
  if plugin_path.exists() and not force:
99
109
  return json.dumps({
100
110
  "ok": False,
@@ -137,6 +147,6 @@ TOOLS = [
137
147
  (
138
148
  handle_personal_plugin_create,
139
149
  "nexo_personal_plugin_create",
140
- "Create a persistent personal MCP plugin scaffold in NEXO_HOME/plugins, optionally with a companion script in NEXO_HOME/scripts.",
150
+ "Create a persistent personal MCP plugin scaffold in NEXO_HOME/personal/plugins, optionally with a companion script in NEXO_HOME/personal/scripts.",
141
151
  ),
142
152
  ]
@@ -8,8 +8,13 @@ from script_registry import (
8
8
  classify_scripts_dir,
9
9
  create_script,
10
10
  ensure_personal_schedules,
11
+ get_automation_status,
12
+ list_operator_automations,
11
13
  reconcile_personal_scripts,
12
14
  remove_personal_script,
15
+ set_automation_enabled,
16
+ set_automation_instructions,
17
+ set_automation_schedule,
13
18
  sync_personal_scripts,
14
19
  unschedule_personal_script,
15
20
  )
@@ -98,23 +103,79 @@ def handle_personal_script_remove(name: str, keep_file: bool = False) -> str:
98
103
  return json.dumps(remove_personal_script(name, keep_file=keep_file), ensure_ascii=False)
99
104
 
100
105
 
106
+ def handle_automations_list(include_all: bool = False) -> str:
107
+ init_db()
108
+ return json.dumps({"ok": True, "automations": list_operator_automations(include_all=include_all)}, ensure_ascii=False)
109
+
110
+
111
+ def handle_automation_status(name: str) -> str:
112
+ init_db()
113
+ return json.dumps(get_automation_status(name), ensure_ascii=False)
114
+
115
+
116
+ def handle_automation_enable(name: str) -> str:
117
+ init_db()
118
+ return json.dumps(set_automation_enabled(name, True), ensure_ascii=False)
119
+
120
+
121
+ def handle_automation_disable(name: str) -> str:
122
+ init_db()
123
+ return json.dumps(set_automation_enabled(name, False), ensure_ascii=False)
124
+
125
+
126
+ def handle_automation_instructions(name: str, text: str = "", clear: bool = False) -> str:
127
+ init_db()
128
+ return json.dumps(set_automation_instructions(name, "" if clear else text), ensure_ascii=False)
129
+
130
+
131
+ def handle_automation_schedule(
132
+ name: str,
133
+ every_seconds: int = 0,
134
+ daily_at: str = "",
135
+ clear: bool = False,
136
+ ) -> str:
137
+ init_db()
138
+ interval_seconds = int(every_seconds or 0) or None
139
+ return json.dumps(
140
+ set_automation_schedule(
141
+ name,
142
+ interval_seconds=interval_seconds,
143
+ daily_at=str(daily_at or "").strip() or None,
144
+ clear=bool(clear),
145
+ ),
146
+ ensure_ascii=False,
147
+ )
148
+
149
+
101
150
  TOOLS = [
102
151
  (handle_personal_scripts_sync, "nexo_personal_scripts_sync",
103
152
  "Sync personal scripts and personal cron schedules from filesystem and LaunchAgents into the registry."),
104
153
  (handle_personal_scripts_classify, "nexo_personal_scripts_classify",
105
- "Classify files in NEXO_HOME/scripts into personal, core, ignored, and non-script buckets."),
154
+ "Classify files in NEXO_HOME/personal/scripts into personal, core, ignored, and non-script buckets."),
106
155
  (handle_personal_scripts_list, "nexo_personal_scripts_list",
107
156
  "List personal scripts known to NEXO, optionally including attached schedules."),
108
157
  (handle_personal_script_create, "nexo_personal_script_create",
109
- "Create a new personal script in NEXO_HOME/scripts, register it, and optionally attach a schedule."),
158
+ "Create a new personal script in NEXO_HOME/personal/scripts, register it, and optionally attach a schedule."),
110
159
  (handle_personal_script_schedules, "nexo_personal_script_schedules",
111
160
  "List registered personal script schedules."),
112
161
  (handle_personal_scripts_reconcile, "nexo_personal_scripts_reconcile",
113
- "Classify, sync, and ensure declared personal schedules so NEXO_HOME/scripts and personal crons stay aligned."),
162
+ "Classify, sync, and ensure declared personal schedules so NEXO_HOME/personal/scripts and personal crons stay aligned."),
114
163
  (handle_personal_scripts_ensure_schedules, "nexo_personal_scripts_ensure_schedules",
115
164
  "Create or repair personal script schedules declared in inline metadata."),
116
165
  (handle_personal_script_unschedule, "nexo_personal_script_unschedule",
117
166
  "Remove all personal schedules attached to a script without touching core crons."),
118
167
  (handle_personal_script_remove, "nexo_personal_script_remove",
119
168
  "Remove a personal script from the registry and optionally delete its file after unscheduling it."),
169
+ (handle_automations_list, "nexo_automations_list",
170
+ "List the operator-facing automations NEXO Desktop manages directly, with optional support/debug widening."),
171
+ (handle_automation_status, "nexo_automation_status",
172
+ "Read the composed runtime status for one automation, including availability, schedule, and operator overrides."),
173
+ (handle_automation_enable, "nexo_automation_enable",
174
+ "Enable one operator-facing automation."),
175
+ (handle_automation_disable, "nexo_automation_disable",
176
+ "Disable one operator-facing automation."),
177
+ (handle_automation_instructions, "nexo_automation_instructions",
178
+ "Set or clear operator-side extra instructions for one automation without editing the core prompt."),
179
+ (handle_automation_schedule, "nexo_automation_schedule",
180
+ "Set or clear the cadence override for one operator-facing automation."),
120
181
  ]
@@ -109,8 +109,8 @@
109
109
  "aliases": [],
110
110
  "metadata": {
111
111
  "old": "~/claude/hooks",
112
- "canonical": "~/.nexo/hooks",
113
- "note": "Pre-v6 runtime lived under ~/claude/*; canonical runtime is ~/.nexo/*"
112
+ "canonical": "~/.nexo/personal/hooks",
113
+ "note": "Pre-v6 runtime lived under ~/claude/*; operator-owned hooks now live under ~/.nexo/personal/*."
114
114
  }
115
115
  },
116
116
  {
@@ -119,7 +119,7 @@
119
119
  "aliases": [],
120
120
  "metadata": {
121
121
  "old": "~/claude/scripts",
122
- "canonical": "~/.nexo/scripts"
122
+ "canonical": "~/.nexo/personal/scripts"
123
123
  }
124
124
  },
125
125
  {
@@ -128,7 +128,70 @@
128
128
  "aliases": [],
129
129
  "metadata": {
130
130
  "old": "~/claude/brain",
131
- "canonical": "~/.nexo/brain"
131
+ "canonical": "~/.nexo/personal/brain"
132
+ }
133
+ },
134
+ {
135
+ "type": "legacy_path",
136
+ "name": "flat_nexo_scripts_to_personal",
137
+ "aliases": [],
138
+ "metadata": {
139
+ "old": "~/.nexo/scripts",
140
+ "canonical": "~/.nexo/personal/scripts"
141
+ }
142
+ },
143
+ {
144
+ "type": "legacy_path",
145
+ "name": "flat_nexo_brain_to_personal",
146
+ "aliases": [],
147
+ "metadata": {
148
+ "old": "~/.nexo/brain",
149
+ "canonical": "~/.nexo/personal/brain"
150
+ }
151
+ },
152
+ {
153
+ "type": "legacy_path",
154
+ "name": "flat_nexo_config_to_personal",
155
+ "aliases": [],
156
+ "metadata": {
157
+ "old": "~/.nexo/config",
158
+ "canonical": "~/.nexo/personal/config"
159
+ }
160
+ },
161
+ {
162
+ "type": "legacy_path",
163
+ "name": "flat_nexo_data_to_runtime",
164
+ "aliases": [],
165
+ "metadata": {
166
+ "old": "~/.nexo/data",
167
+ "canonical": "~/.nexo/runtime/data"
168
+ }
169
+ },
170
+ {
171
+ "type": "legacy_path",
172
+ "name": "flat_nexo_logs_to_runtime",
173
+ "aliases": [],
174
+ "metadata": {
175
+ "old": "~/.nexo/logs",
176
+ "canonical": "~/.nexo/runtime/logs"
177
+ }
178
+ },
179
+ {
180
+ "type": "legacy_path",
181
+ "name": "flat_nexo_operations_to_runtime",
182
+ "aliases": [],
183
+ "metadata": {
184
+ "old": "~/.nexo/operations",
185
+ "canonical": "~/.nexo/runtime/operations"
186
+ }
187
+ },
188
+ {
189
+ "type": "legacy_path",
190
+ "name": "flat_nexo_exports_to_runtime",
191
+ "aliases": [],
192
+ "metadata": {
193
+ "old": "~/.nexo/exports",
194
+ "canonical": "~/.nexo/runtime/exports"
132
195
  }
133
196
  },
134
197
  {