nexo-brain 6.4.0 → 7.0.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/src/paths.py ADDED
@@ -0,0 +1,394 @@
1
+ """Plan Consolidado F0.6 — canonical path helpers.
2
+
3
+ Every module that needs to find a runtime directory should import from
4
+ here instead of hardcoding `NEXO_HOME / "scripts"` etc. The legacy
5
+ flat layout (`~/.nexo/scripts/`, `~/.nexo/brain/`, `~/.nexo/data/`,
6
+ ...) is going away in v7.0.0; this module centralises the new tree
7
+ so the migration is a one-line change per call site.
8
+
9
+ New structure (post-F0.6):
10
+ ~/.nexo/
11
+ ├── core/ ← shipped with the package
12
+ │ ├── scripts/ (38 packaged automations)
13
+ │ ├── plugins/
14
+ │ ├── hooks/
15
+ │ ├── rules/
16
+ │ └── contracts/ (resonance_tiers.json, ...)
17
+ ├── core-dev/ ← dev-only, off by default
18
+ │ └── scripts/
19
+ ├── personal/ ← operator. nexo update never touches.
20
+ │ ├── scripts/
21
+ │ ├── skills/
22
+ │ ├── plugins/
23
+ │ ├── hooks/
24
+ │ ├── rules/
25
+ │ ├── brain/ (calibration.json, project-atlas.json,
26
+ │ │ operator-routing-rules.json, ...)
27
+ │ ├── config/
28
+ │ ├── lib/
29
+ │ └── overrides/
30
+ └── runtime/ ← dynamic state
31
+ ├── data/ (nexo.db, *.db)
32
+ ├── logs/
33
+ ├── operations/
34
+ ├── backups/
35
+ ├── memory/
36
+ ├── cognitive/
37
+ ├── coordination/
38
+ ├── exports/
39
+ ├── nexo-email/
40
+ ├── doctor/
41
+ ├── snapshots/
42
+ └── crons/
43
+
44
+ Backwards compatibility: every helper has `legacy=True` mode that
45
+ returns the pre-F0.6 location (`NEXO_HOME / "<name>"`). The compat
46
+ layer disappears in v7.1.0; v7.0.0 keeps it so operator-edited code
47
+ that hardcoded the old paths keeps resolving via symlink during the
48
+ 1-week observation window.
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ import os
54
+ from pathlib import Path
55
+
56
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
57
+
58
+
59
+ def home() -> Path:
60
+ """Return the active NEXO_HOME (recomputed every call so tests
61
+ that monkeypatch the env var see the right path)."""
62
+ return Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Core (shipped with the package, replaced on every `nexo update`)
67
+ # ---------------------------------------------------------------------------
68
+ def core_dir() -> Path:
69
+ return home() / "core"
70
+
71
+
72
+ def core_scripts_dir() -> Path:
73
+ new = core_dir() / "scripts"
74
+ legacy = home() / "scripts"
75
+ if not new.exists() and legacy.exists():
76
+ return legacy
77
+ return new
78
+
79
+
80
+ def core_plugins_dir() -> Path:
81
+ new = core_dir() / "plugins"
82
+ legacy = home() / "plugins"
83
+ if not new.exists() and legacy.exists():
84
+ return legacy
85
+ return new
86
+
87
+
88
+ def core_hooks_dir() -> Path:
89
+ new = core_dir() / "hooks"
90
+ legacy = home() / "hooks"
91
+ if not new.exists() and legacy.exists():
92
+ return legacy
93
+ return new
94
+
95
+
96
+ def core_rules_dir() -> Path:
97
+ new = core_dir() / "rules"
98
+ legacy = home() / "rules"
99
+ if not new.exists() and legacy.exists():
100
+ return legacy
101
+ return new
102
+
103
+
104
+ def core_contracts_dir() -> Path:
105
+ return core_dir() / "contracts"
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Core-dev (off by default, only useful to product devs)
110
+ # ---------------------------------------------------------------------------
111
+ def core_dev_dir() -> Path:
112
+ return home() / "core-dev"
113
+
114
+
115
+ def core_dev_scripts_dir() -> Path:
116
+ return core_dev_dir() / "scripts"
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Personal (operator-owned, `nexo update` never touches)
121
+ # ---------------------------------------------------------------------------
122
+ def personal_dir() -> Path:
123
+ return home() / "personal"
124
+
125
+
126
+ def personal_scripts_dir() -> Path:
127
+ return personal_dir() / "scripts"
128
+
129
+
130
+ def personal_plugins_dir() -> Path:
131
+ return personal_dir() / "plugins"
132
+
133
+
134
+ def personal_hooks_dir() -> Path:
135
+ return personal_dir() / "hooks"
136
+
137
+
138
+ def personal_rules_dir() -> Path:
139
+ return personal_dir() / "rules"
140
+
141
+
142
+ def personal_skills_dir() -> Path:
143
+ new = personal_dir() / "skills"
144
+ legacy = home() / "skills"
145
+ if not new.exists() and legacy.exists():
146
+ return legacy
147
+ return new
148
+
149
+
150
+ def brain_dir() -> Path:
151
+ """Operator brain: calibration, project atlas, routing rules, ..."""
152
+ new = personal_dir() / "brain"
153
+ legacy = home() / "brain"
154
+ if not new.exists() and legacy.exists():
155
+ return legacy
156
+ return new
157
+
158
+
159
+ def personal_config_dir() -> Path:
160
+ return personal_dir() / "config"
161
+
162
+
163
+ def personal_lib_dir() -> Path:
164
+ return personal_dir() / "lib"
165
+
166
+
167
+ def personal_overrides_dir() -> Path:
168
+ return personal_dir() / "overrides"
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # Runtime (dynamic state, never edited by hand)
173
+ # ---------------------------------------------------------------------------
174
+ def runtime_dir() -> Path:
175
+ return home() / "runtime"
176
+
177
+
178
+ def data_dir() -> Path:
179
+ new = runtime_dir() / "data"
180
+ legacy = home() / "data"
181
+ if not new.exists() and legacy.exists():
182
+ return legacy
183
+ return new
184
+
185
+
186
+ def db_path() -> Path:
187
+ new = data_dir() / "nexo.db"
188
+ legacy = home() / "data" / "nexo.db"
189
+ if not new.is_file() and legacy.is_file():
190
+ return legacy
191
+ return new
192
+
193
+
194
+ def logs_dir() -> Path:
195
+ new = runtime_dir() / "logs"
196
+ legacy = home() / "logs"
197
+ if not new.exists() and legacy.exists():
198
+ return legacy
199
+ return new
200
+
201
+
202
+ def operations_dir() -> Path:
203
+ new = runtime_dir() / "operations"
204
+ legacy = home() / "operations"
205
+ if not new.exists() and legacy.exists():
206
+ return legacy
207
+ return new
208
+
209
+
210
+ def backups_dir() -> Path:
211
+ new = runtime_dir() / "backups"
212
+ legacy = home() / "backups"
213
+ if not new.exists() and legacy.exists():
214
+ return legacy
215
+ return new
216
+
217
+
218
+ def memory_dir() -> Path:
219
+ new = runtime_dir() / "memory"
220
+ legacy = home() / "memory"
221
+ if not new.exists() and legacy.exists():
222
+ return legacy
223
+ return new
224
+
225
+
226
+ def cognitive_dir() -> Path:
227
+ new = runtime_dir() / "cognitive"
228
+ legacy = home() / "cognitive"
229
+ if not new.exists() and legacy.exists():
230
+ return legacy
231
+ return new
232
+
233
+
234
+ def coordination_dir() -> Path:
235
+ new = runtime_dir() / "coordination"
236
+ legacy = home() / "coordination"
237
+ if not new.exists() and legacy.exists():
238
+ return legacy
239
+ return new
240
+
241
+
242
+ def exports_dir() -> Path:
243
+ new = runtime_dir() / "exports"
244
+ legacy = home() / "exports"
245
+ if not new.exists() and legacy.exists():
246
+ return legacy
247
+ return new
248
+
249
+
250
+ def nexo_email_dir() -> Path:
251
+ new = runtime_dir() / "nexo-email"
252
+ legacy = home() / "nexo-email"
253
+ if not new.exists() and legacy.exists():
254
+ return legacy
255
+ return new
256
+
257
+
258
+ def doctor_dir() -> Path:
259
+ new = runtime_dir() / "doctor"
260
+ legacy = home() / "doctor"
261
+ if not new.exists() and legacy.exists():
262
+ return legacy
263
+ return new
264
+
265
+
266
+ def snapshots_dir() -> Path:
267
+ new = runtime_dir() / "snapshots"
268
+ legacy = home() / "snapshots"
269
+ if not new.exists() and legacy.exists():
270
+ return legacy
271
+ return new
272
+
273
+
274
+ def crons_dir() -> Path:
275
+ new = runtime_dir() / "crons"
276
+ legacy = home() / "crons"
277
+ if not new.exists() and legacy.exists():
278
+ return legacy
279
+ return new
280
+
281
+
282
+ # ---------------------------------------------------------------------------
283
+ # Combined views (for callers that need to scan core+personal merged)
284
+ # ---------------------------------------------------------------------------
285
+ def all_scripts_dirs() -> list[Path]:
286
+ """Return every directory `nexo scripts list` should scan."""
287
+ return [core_scripts_dir(), personal_scripts_dir(), core_dev_scripts_dir()]
288
+
289
+
290
+ def all_plugins_dirs() -> list[Path]:
291
+ return [core_plugins_dir(), personal_plugins_dir()]
292
+
293
+
294
+ def all_hooks_dirs() -> list[Path]:
295
+ return [core_hooks_dir(), personal_hooks_dir()]
296
+
297
+
298
+ def all_rules_dirs() -> list[Path]:
299
+ return [core_rules_dir(), personal_rules_dir()]
300
+
301
+
302
+ # ---------------------------------------------------------------------------
303
+ # Legacy compat (PRE-F0.6 paths). Every shipped runtime keeps these as
304
+ # symlinks to the new locations until v7.1.0, so operator code that
305
+ # hardcoded the flat layout continues to resolve.
306
+ # ---------------------------------------------------------------------------
307
+ def legacy_scripts_dir() -> Path:
308
+ return home() / "scripts"
309
+
310
+
311
+ def legacy_brain_dir() -> Path:
312
+ return home() / "brain"
313
+
314
+
315
+ def legacy_data_dir() -> Path:
316
+ return home() / "data"
317
+
318
+
319
+ def legacy_logs_dir() -> Path:
320
+ return home() / "logs"
321
+
322
+
323
+ def legacy_operations_dir() -> Path:
324
+ return home() / "operations"
325
+
326
+
327
+ def legacy_db_path() -> Path:
328
+ return legacy_data_dir() / "nexo.db"
329
+
330
+
331
+ # ---------------------------------------------------------------------------
332
+ # Smart resolver: prefer new location if it exists, fall back to legacy.
333
+ # Used during the v7.0.0 / v7.1.0 transition window.
334
+ # ---------------------------------------------------------------------------
335
+ def resolve_db_path() -> Path:
336
+ """Return the active SQLite DB path, preferring the new location
337
+ but falling back to the legacy one when an older runtime hasn't
338
+ migrated yet."""
339
+ new = db_path()
340
+ if new.is_file():
341
+ return new
342
+ legacy = legacy_db_path()
343
+ if legacy.is_file():
344
+ return legacy
345
+ return new # default: new layout for fresh installs
346
+
347
+
348
+ __all__ = [
349
+ "NEXO_HOME",
350
+ "home",
351
+ "core_dir",
352
+ "core_scripts_dir",
353
+ "core_plugins_dir",
354
+ "core_hooks_dir",
355
+ "core_rules_dir",
356
+ "core_contracts_dir",
357
+ "core_dev_dir",
358
+ "core_dev_scripts_dir",
359
+ "personal_dir",
360
+ "personal_scripts_dir",
361
+ "personal_plugins_dir",
362
+ "personal_hooks_dir",
363
+ "personal_rules_dir",
364
+ "personal_skills_dir",
365
+ "brain_dir",
366
+ "personal_config_dir",
367
+ "personal_lib_dir",
368
+ "personal_overrides_dir",
369
+ "runtime_dir",
370
+ "data_dir",
371
+ "db_path",
372
+ "logs_dir",
373
+ "operations_dir",
374
+ "backups_dir",
375
+ "memory_dir",
376
+ "cognitive_dir",
377
+ "coordination_dir",
378
+ "exports_dir",
379
+ "nexo_email_dir",
380
+ "doctor_dir",
381
+ "snapshots_dir",
382
+ "crons_dir",
383
+ "all_scripts_dirs",
384
+ "all_plugins_dirs",
385
+ "all_hooks_dirs",
386
+ "all_rules_dirs",
387
+ "legacy_scripts_dir",
388
+ "legacy_brain_dir",
389
+ "legacy_data_dir",
390
+ "legacy_logs_dir",
391
+ "legacy_operations_dir",
392
+ "legacy_db_path",
393
+ "resolve_db_path",
394
+ ]
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import os
7
+ import paths
7
8
  from pathlib import Path
8
9
 
9
10
  from db import init_db
@@ -15,7 +16,7 @@ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent
15
16
 
16
17
 
17
18
  def _plugins_dir() -> Path:
18
- path = NEXO_HOME / "plugins"
19
+ path = paths.core_plugins_dir()
19
20
  path.mkdir(parents=True, exist_ok=True)
20
21
  return path
21
22
 
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import json
21
21
  import os
22
+ import paths
22
23
  import re
23
24
  import sys
24
25
  import time
@@ -46,8 +47,8 @@ from db_guard import (
46
47
  )
47
48
 
48
49
  NEXO_HOME = export_resolved_nexo_home()
49
- DATA_DIR = NEXO_HOME / "data"
50
- BACKUP_BASE = NEXO_HOME / "backups"
50
+ DATA_DIR = paths.data_dir()
51
+ BACKUP_BASE = paths.backups_dir()
51
52
  PRIMARY_DB = DATA_DIR / "nexo.db"
52
53
 
53
54
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
  """Update plugin — pull latest code, backup DBs, run migrations, verify."""
3
3
  import json
4
4
  import os
5
+ import paths
5
6
  import re
6
7
  import shutil
7
8
  import sqlite3
@@ -84,8 +85,8 @@ CODE_ROOT = _THIS_DIR.parent
84
85
  _REPO_CANDIDATE = CODE_ROOT.parent
85
86
 
86
87
  NEXO_HOME = export_resolved_nexo_home()
87
- DATA_DIR = NEXO_HOME / "data"
88
- BACKUP_BASE = NEXO_HOME / "backups"
88
+ DATA_DIR = paths.data_dir()
89
+ BACKUP_BASE = paths.backups_dir()
89
90
 
90
91
  # In packaged installs, update.py lives at <NEXO_HOME>/plugins/update.py.
91
92
  _PACKAGED_INSTALL = not (_REPO_CANDIDATE / ".git").exists() and not (_REPO_CANDIDATE / ".git").is_file()
@@ -161,7 +162,7 @@ def _refresh_installed_manifest():
161
162
  return
162
163
 
163
164
  src_crons = artifact_src / "crons"
164
- dst_crons = NEXO_HOME / "crons"
165
+ dst_crons = paths.crons_dir()
165
166
  if src_crons.exists():
166
167
  dst_crons.mkdir(parents=True, exist_ok=True)
167
168
  for f in src_crons.iterdir():
@@ -193,10 +194,10 @@ def _refresh_installed_manifest():
193
194
  def _cleanup_retired_runtime_files() -> list[str]:
194
195
  removed: list[str] = []
195
196
  retired_paths = [
196
- NEXO_HOME / "scripts" / "heartbeat-enforcement.py",
197
- NEXO_HOME / "scripts" / "heartbeat-posttool.sh",
198
- NEXO_HOME / "scripts" / "heartbeat-user-msg.sh",
199
- NEXO_HOME / "hooks" / "heartbeat-guard.sh",
197
+ paths.core_scripts_dir() / "heartbeat-enforcement.py",
198
+ paths.core_scripts_dir() / "heartbeat-posttool.sh",
199
+ paths.core_scripts_dir() / "heartbeat-user-msg.sh",
200
+ paths.core_hooks_dir() / "heartbeat-guard.sh",
200
201
  ]
201
202
  for path in retired_paths:
202
203
  if not path.exists():
@@ -723,7 +724,7 @@ def _sync_hooks_to_home():
723
724
  """Copy hook scripts from src/hooks/ to NEXO_HOME/hooks/ after update."""
724
725
  import shutil
725
726
  hooks_src = SRC_DIR / "hooks"
726
- hooks_dest = NEXO_HOME / "hooks"
727
+ hooks_dest = paths.core_hooks_dir()
727
728
  if not hooks_src.is_dir():
728
729
  return
729
730
  hooks_dest.mkdir(parents=True, exist_ok=True)
@@ -893,7 +894,7 @@ def _paths_match(src: Path, dest: Path) -> bool:
893
894
 
894
895
 
895
896
  def _sync_packaged_crons(progress_fn=None) -> tuple[bool, str | None]:
896
- sync_path = NEXO_HOME / "crons" / "sync.py"
897
+ sync_path = paths.crons_dir() / "sync.py"
897
898
  if not sync_path.is_file():
898
899
  _refresh_installed_manifest()
899
900
  return True, None
@@ -9,6 +9,7 @@ This module manages the opt-in "public core evolution" mode:
9
9
 
10
10
  import json
11
11
  import os
12
+ import paths
12
13
  import platform
13
14
  import re
14
15
  import shutil
@@ -47,7 +48,7 @@ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
47
48
  CONTRIB_ROOT = NEXO_HOME / "contrib" / "public-core"
48
49
  CONTRIB_REPO_DIR = CONTRIB_ROOT / "repo"
49
50
  CONTRIB_WORKTREES_DIR = CONTRIB_ROOT / "worktrees"
50
- CONTRIB_ARTIFACTS_DIR = NEXO_HOME / "operations" / "public-contrib"
51
+ CONTRIB_ARTIFACTS_DIR = paths.operations_dir() / "public-contrib"
51
52
 
52
53
 
53
54
  def _utcnow() -> datetime:
@@ -15,6 +15,7 @@ Important semantic note:
15
15
 
16
16
  import json
17
17
  import os
18
+ import paths
18
19
  import platform
19
20
  import plistlib
20
21
  import shutil
@@ -336,7 +337,7 @@ def detect_full_disk_access_reasons(*, system: str | None = None) -> list[str]:
336
337
  f"NEXO_HOME is inside a protected macOS folder: {NEXO_HOME}"
337
338
  )
338
339
 
339
- logs_dir = NEXO_HOME / "logs"
340
+ logs_dir = paths.logs_dir()
340
341
  if logs_dir.is_dir():
341
342
  for log_file in sorted(logs_dir.glob("*-stderr.log")):
342
343
  if _tail_has_permission_denial(log_file):
@@ -715,7 +716,7 @@ def ensure_full_disk_access_choice(
715
716
 
716
717
 
717
718
  def _prevent_sleep_script_path() -> Path:
718
- runtime_script = NEXO_HOME / "scripts" / "nexo-prevent-sleep.sh"
719
+ runtime_script = paths.core_scripts_dir() / "nexo-prevent-sleep.sh"
719
720
  if runtime_script.is_file():
720
721
  return runtime_script
721
722
  source_script = NEXO_CODE / "scripts" / "nexo-prevent-sleep.sh"
@@ -730,8 +731,8 @@ def _macos_prevent_sleep_plist() -> tuple[Path, dict]:
730
731
  "ProgramArguments": ["/bin/bash", str(script_path)],
731
732
  "RunAtLoad": True,
732
733
  "KeepAlive": True,
733
- "StandardOutPath": str(NEXO_HOME / "logs" / "prevent-sleep-stdout.log"),
734
- "StandardErrorPath": str(NEXO_HOME / "logs" / "prevent-sleep-stderr.log"),
734
+ "StandardOutPath": str(paths.logs_dir() / "prevent-sleep-stdout.log"),
735
+ "StandardErrorPath": str(paths.logs_dir() / "prevent-sleep-stderr.log"),
735
736
  "EnvironmentVariables": {
736
737
  "HOME": str(Path.home()),
737
738
  "NEXO_HOME": str(NEXO_HOME),
@@ -766,7 +767,7 @@ WantedBy=default.target
766
767
  def apply_power_policy(policy: str | None = None) -> dict:
767
768
  policy = normalize_power_policy(policy or get_power_policy())
768
769
  system = platform.system()
769
- logs_dir = NEXO_HOME / "logs"
770
+ logs_dir = paths.logs_dir()
770
771
  logs_dir.mkdir(parents=True, exist_ok=True)
771
772
  details = describe_power_policy(policy=policy, system=system)
772
773