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/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -1
- package/package.json +2 -2
- package/src/auto_update.py +271 -30
- package/src/bootstrap_docs.py +2 -1
- package/src/calibration_migration.py +5 -2
- package/src/cli.py +69 -6
- package/src/cron_recovery.py +4 -3
- package/src/db/_personal_scripts.py +3 -1
- package/src/db/_skills.py +2 -1
- package/src/desktop_bridge.py +4 -2
- package/src/doctor/providers/boot.py +21 -7
- package/src/doctor/providers/deep.py +6 -5
- package/src/doctor/providers/runtime.py +17 -16
- package/src/evolution_cycle.py +7 -6
- package/src/health_check.py +4 -2
- package/src/paths.py +394 -0
- package/src/plugins/personal_plugins.py +2 -1
- package/src/plugins/recover.py +3 -2
- package/src/plugins/update.py +10 -9
- package/src/public_contribution.py +2 -1
- package/src/runtime_power.py +6 -5
- package/src/script_registry.py +154 -20
- package/src/scripts/nexo-backup.sh +2 -2
- package/src/scripts/nexo-cron-wrapper.sh +40 -2
- package/src/scripts/nexo-deep-sleep.sh +2 -2
- package/src/scripts/nexo-inbox-hook.sh +1 -1
- package/src/scripts/nexo-snapshot-restore.sh +1 -1
- package/src/scripts/nexo-tcc-approve.sh +2 -2
- package/src/scripts/nexo-watchdog.sh +14 -14
- package/src/system_catalog.py +3 -2
- package/src/tools_sessions.py +2 -1
- package/src/user_data_portability.py +9 -8
package/src/cli.py
CHANGED
|
@@ -37,6 +37,7 @@ import contextlib
|
|
|
37
37
|
import io
|
|
38
38
|
import json
|
|
39
39
|
import os
|
|
40
|
+
import paths
|
|
40
41
|
import shutil
|
|
41
42
|
import subprocess
|
|
42
43
|
import sys
|
|
@@ -303,7 +304,7 @@ def _scripts_list(args):
|
|
|
303
304
|
print(json.dumps(scripts, indent=2))
|
|
304
305
|
else:
|
|
305
306
|
if not scripts:
|
|
306
|
-
print("No personal scripts found in",
|
|
307
|
+
print("No personal scripts found in", paths.core_scripts_dir())
|
|
307
308
|
return 0
|
|
308
309
|
# Table output
|
|
309
310
|
name_w = max(len(s["name"]) for s in scripts)
|
|
@@ -347,7 +348,7 @@ def _scripts_classify(args):
|
|
|
347
348
|
|
|
348
349
|
entries = report.get("entries", [])
|
|
349
350
|
if not entries:
|
|
350
|
-
print("No scripts directory found:", report.get("scripts_dir",
|
|
351
|
+
print("No scripts directory found:", report.get("scripts_dir", paths.core_scripts_dir()))
|
|
351
352
|
return 0
|
|
352
353
|
|
|
353
354
|
path_w = max(len(Path(entry["path"]).name) for entry in entries)
|
|
@@ -452,6 +453,49 @@ def _scripts_unschedule(args):
|
|
|
452
453
|
return 0 if result.get("ok") else 1
|
|
453
454
|
|
|
454
455
|
|
|
456
|
+
def _scripts_set_enabled(args, enabled):
|
|
457
|
+
from script_registry import set_personal_script_enabled
|
|
458
|
+
|
|
459
|
+
result = set_personal_script_enabled(args.name, enabled)
|
|
460
|
+
if args.json:
|
|
461
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
462
|
+
return 0 if result.get("ok") else 1
|
|
463
|
+
if not result.get("ok"):
|
|
464
|
+
print(result.get("error", "Failed to toggle script"), file=sys.stderr)
|
|
465
|
+
return 1
|
|
466
|
+
verb = "enabled" if enabled else "disabled"
|
|
467
|
+
if result.get("changed"):
|
|
468
|
+
print(f"Script {result['name']} {verb}.")
|
|
469
|
+
else:
|
|
470
|
+
print(f"Script {result['name']} already {verb}.")
|
|
471
|
+
return 0
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _scripts_status(args):
|
|
475
|
+
from script_registry import get_personal_script_status
|
|
476
|
+
|
|
477
|
+
result = get_personal_script_status(args.name)
|
|
478
|
+
if args.json:
|
|
479
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
480
|
+
return 0 if result.get("ok") else 1
|
|
481
|
+
if not result.get("ok"):
|
|
482
|
+
print(result.get("error", "Failed to read status"), file=sys.stderr)
|
|
483
|
+
return 1
|
|
484
|
+
state = "enabled" if result.get("enabled") else "DISABLED"
|
|
485
|
+
print(f"{result.get('name')} [{result.get('classification')}] -> {state}")
|
|
486
|
+
last = result.get("last_run") or {}
|
|
487
|
+
if last:
|
|
488
|
+
exit_code = last.get("exit_code")
|
|
489
|
+
started = last.get("started_at") or "?"
|
|
490
|
+
print(f" last run: {started} (exit={exit_code})")
|
|
491
|
+
summary = (last.get("summary") or "").strip()
|
|
492
|
+
if summary:
|
|
493
|
+
print(f" summary: {summary[:120]}")
|
|
494
|
+
else:
|
|
495
|
+
print(" last run: (none)")
|
|
496
|
+
return 0
|
|
497
|
+
|
|
498
|
+
|
|
455
499
|
def _scripts_remove(args):
|
|
456
500
|
from script_registry import remove_personal_script
|
|
457
501
|
|
|
@@ -495,8 +539,8 @@ def _scripts_run(args):
|
|
|
495
539
|
|
|
496
540
|
# Only inject DB paths for core scripts
|
|
497
541
|
if is_core:
|
|
498
|
-
env["NEXO_DB"] = str(
|
|
499
|
-
env["NEXO_COGNITIVE_DB"] = str(
|
|
542
|
+
env["NEXO_DB"] = str(paths.db_path())
|
|
543
|
+
env["NEXO_COGNITIVE_DB"] = str(paths.data_dir() / "cognitive.db")
|
|
500
544
|
|
|
501
545
|
# Timeout
|
|
502
546
|
timeout = None
|
|
@@ -988,7 +1032,7 @@ def _update(args):
|
|
|
988
1032
|
# the sensory-register buffer. Keeps a .pre-v5.4.1.bak backup the first
|
|
989
1033
|
# time it runs on a given host.
|
|
990
1034
|
try:
|
|
991
|
-
buf =
|
|
1035
|
+
buf = paths.brain_dir() / "session_buffer.jsonl"
|
|
992
1036
|
marker = buf.with_suffix(".jsonl.pre-v5.4.1.bak")
|
|
993
1037
|
if buf.is_file() and not marker.is_file():
|
|
994
1038
|
raw = buf.read_text(errors="ignore").splitlines()
|
|
@@ -1120,7 +1164,7 @@ def _write_calibration_default_resonance(tier: str) -> None:
|
|
|
1120
1164
|
…). This helper keeps the CLI path writing to both calibration.json
|
|
1121
1165
|
AND schedule.json so the two surfaces never disagree.
|
|
1122
1166
|
"""
|
|
1123
|
-
cal_path =
|
|
1167
|
+
cal_path = paths.brain_dir() / "calibration.json"
|
|
1124
1168
|
try:
|
|
1125
1169
|
cal_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1126
1170
|
if cal_path.exists():
|
|
@@ -2116,6 +2160,19 @@ def main():
|
|
|
2116
2160
|
unschedule_p.add_argument("name", help="Script name or path")
|
|
2117
2161
|
unschedule_p.add_argument("--json", action="store_true", help="JSON output")
|
|
2118
2162
|
|
|
2163
|
+
# scripts enable / disable / status (Plan F0.2.2)
|
|
2164
|
+
enable_p = scripts_sub.add_parser("enable", help="Enable a personal script (cron wrapper will run it again)")
|
|
2165
|
+
enable_p.add_argument("name", help="Script name or path")
|
|
2166
|
+
enable_p.add_argument("--json", action="store_true", help="JSON output")
|
|
2167
|
+
|
|
2168
|
+
disable_p = scripts_sub.add_parser("disable", help="Disable a personal script (cron wrapper will skip it)")
|
|
2169
|
+
disable_p.add_argument("name", help="Script name or path")
|
|
2170
|
+
disable_p.add_argument("--json", action="store_true", help="JSON output")
|
|
2171
|
+
|
|
2172
|
+
status_p = scripts_sub.add_parser("status", help="Show enabled flag + last cron_runs row for a script")
|
|
2173
|
+
status_p.add_argument("name", help="Script name or path")
|
|
2174
|
+
status_p.add_argument("--json", action="store_true", help="JSON output")
|
|
2175
|
+
|
|
2119
2176
|
# scripts remove
|
|
2120
2177
|
remove_p = scripts_sub.add_parser("remove", help="Remove a personal script and any attached schedules")
|
|
2121
2178
|
remove_p.add_argument("name", help="Script name or path")
|
|
@@ -2394,6 +2451,12 @@ def main():
|
|
|
2394
2451
|
return _scripts_doctor(args)
|
|
2395
2452
|
elif args.scripts_command == "call":
|
|
2396
2453
|
return _scripts_call(args)
|
|
2454
|
+
elif args.scripts_command == "enable":
|
|
2455
|
+
return _scripts_set_enabled(args, True)
|
|
2456
|
+
elif args.scripts_command == "disable":
|
|
2457
|
+
return _scripts_set_enabled(args, False)
|
|
2458
|
+
elif args.scripts_command == "status":
|
|
2459
|
+
return _scripts_status(args)
|
|
2397
2460
|
else:
|
|
2398
2461
|
scripts_parser.print_help()
|
|
2399
2462
|
return 0
|
package/src/cron_recovery.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import paths
|
|
6
7
|
import plistlib
|
|
7
8
|
import sqlite3
|
|
8
9
|
import contextlib
|
|
@@ -16,8 +17,8 @@ NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent
|
|
|
16
17
|
LAUNCH_AGENTS_DIR = Path.home() / "Library" / "LaunchAgents"
|
|
17
18
|
OPTIONALS_FILE = NEXO_HOME / "config" / "optionals.json"
|
|
18
19
|
SCHEDULE_FILE = NEXO_HOME / "config" / "schedule.json"
|
|
19
|
-
DB_PATH =
|
|
20
|
-
STATE_FILE =
|
|
20
|
+
DB_PATH = paths.db_path()
|
|
21
|
+
STATE_FILE = paths.operations_dir() / ".catchup-state.json"
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def _local_timezone():
|
|
@@ -84,7 +85,7 @@ def resolve_declared_schedule(cron: dict) -> dict:
|
|
|
84
85
|
|
|
85
86
|
def load_enabled_crons() -> list[dict]:
|
|
86
87
|
manifest_candidates = [
|
|
87
|
-
|
|
88
|
+
paths.crons_dir() / "manifest.json",
|
|
88
89
|
NEXO_CODE / "crons" / "manifest.json",
|
|
89
90
|
]
|
|
90
91
|
optionals = _load_json(OPTIONALS_FILE, {})
|
|
@@ -122,7 +122,9 @@ def upsert_personal_script(
|
|
|
122
122
|
metadata_json = excluded.metadata_json,
|
|
123
123
|
created_by = COALESCE(NULLIF(personal_scripts.created_by, ''), excluded.created_by),
|
|
124
124
|
source = excluded.source,
|
|
125
|
-
enabled
|
|
125
|
+
-- Plan F0.2.2: preserve operator-set `enabled` flag across sync runs.
|
|
126
|
+
-- Sync defaults to enabled=True for INSERTs; on UPDATE we keep
|
|
127
|
+
-- whatever the operator (or `nexo scripts disable`) set.
|
|
126
128
|
has_inline_metadata = excluded.has_inline_metadata,
|
|
127
129
|
last_synced_at = excluded.last_synced_at,
|
|
128
130
|
updated_at = excluded.updated_at
|
package/src/db/_skills.py
CHANGED
|
@@ -11,6 +11,7 @@ Executable skills are indexed in SQLite but sourced from filesystem definitions.
|
|
|
11
11
|
import datetime
|
|
12
12
|
import json
|
|
13
13
|
import os
|
|
14
|
+
import paths
|
|
14
15
|
import re
|
|
15
16
|
import shutil
|
|
16
17
|
from pathlib import Path
|
|
@@ -25,7 +26,7 @@ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
|
25
26
|
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parents[1])))
|
|
26
27
|
|
|
27
28
|
NEXO_ROOT = NEXO_CODE.parent
|
|
28
|
-
PERSONAL_SKILLS_DIR =
|
|
29
|
+
PERSONAL_SKILLS_DIR = paths.personal_skills_dir()
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def _resolve_core_skills_dir() -> Path:
|
package/src/desktop_bridge.py
CHANGED
|
@@ -27,11 +27,13 @@ def _nexo_home() -> Path:
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _calibration_path() -> Path:
|
|
30
|
-
|
|
30
|
+
import paths
|
|
31
|
+
return paths.brain_dir() / "calibration.json"
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def _profile_path() -> Path:
|
|
34
|
-
|
|
35
|
+
import paths
|
|
36
|
+
return paths.brain_dir() / "profile.json"
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
def _read_json(path: Path) -> dict:
|
|
@@ -13,7 +13,8 @@ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
|
13
13
|
|
|
14
14
|
def check_db_exists() -> DoctorCheck:
|
|
15
15
|
"""Check that the main database file exists and is readable."""
|
|
16
|
-
|
|
16
|
+
import paths
|
|
17
|
+
db_path = paths.db_path()
|
|
17
18
|
if db_path.is_file():
|
|
18
19
|
size_kb = db_path.stat().st_size / 1024
|
|
19
20
|
return DoctorCheck(
|
|
@@ -37,9 +38,21 @@ def check_db_exists() -> DoctorCheck:
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
def check_required_dirs() -> DoctorCheck:
|
|
40
|
-
"""Check that required NEXO_HOME directories exist.
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
"""Check that required NEXO_HOME directories exist (post-F0.6 layout
|
|
42
|
+
or pre-F0.6 fallback)."""
|
|
43
|
+
import paths
|
|
44
|
+
# Each required dir maps to one of the new locations OR the legacy fallback.
|
|
45
|
+
required_helpers = [
|
|
46
|
+
("data", paths.data_dir()),
|
|
47
|
+
("scripts", paths.core_scripts_dir()),
|
|
48
|
+
("plugins", paths.core_plugins_dir()),
|
|
49
|
+
("crons", paths.crons_dir()),
|
|
50
|
+
("hooks", paths.core_hooks_dir()),
|
|
51
|
+
("coordination", paths.coordination_dir()),
|
|
52
|
+
("operations", paths.operations_dir()),
|
|
53
|
+
("logs", paths.logs_dir()),
|
|
54
|
+
]
|
|
55
|
+
missing = [name for (name, path) in required_helpers if not path.is_dir()]
|
|
43
56
|
|
|
44
57
|
if not missing:
|
|
45
58
|
return DoctorCheck(
|
|
@@ -47,7 +60,7 @@ def check_required_dirs() -> DoctorCheck:
|
|
|
47
60
|
tier="boot",
|
|
48
61
|
status="healthy",
|
|
49
62
|
severity="info",
|
|
50
|
-
summary=f"All {len(
|
|
63
|
+
summary=f"All {len(required_helpers)} required directories present",
|
|
51
64
|
)
|
|
52
65
|
|
|
53
66
|
return DoctorCheck(
|
|
@@ -57,7 +70,7 @@ def check_required_dirs() -> DoctorCheck:
|
|
|
57
70
|
severity="warn" if len(missing) < 3 else "error",
|
|
58
71
|
summary=f"{len(missing)} required director{'y' if len(missing) == 1 else 'ies'} missing",
|
|
59
72
|
evidence=[f"Missing: {d}" for d in missing],
|
|
60
|
-
repair_plan=[f"mkdir -p {
|
|
73
|
+
repair_plan=[f"mkdir -p {dict(required_helpers)[d]}" for d in missing],
|
|
61
74
|
)
|
|
62
75
|
|
|
63
76
|
|
|
@@ -107,7 +120,8 @@ def check_disk_space() -> DoctorCheck:
|
|
|
107
120
|
|
|
108
121
|
def check_wrapper_scripts() -> DoctorCheck:
|
|
109
122
|
"""Check that cron wrapper script exists."""
|
|
110
|
-
|
|
123
|
+
import paths
|
|
124
|
+
wrapper = paths.core_scripts_dir() / "nexo-cron-wrapper.sh"
|
|
111
125
|
if wrapper.is_file():
|
|
112
126
|
return DoctorCheck(
|
|
113
127
|
id="boot.wrapper_scripts",
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import datetime as dt
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import paths
|
|
7
8
|
import time
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
@@ -67,7 +68,7 @@ def _self_audit_enabled() -> bool | None:
|
|
|
67
68
|
|
|
68
69
|
def check_self_audit_summary() -> DoctorCheck:
|
|
69
70
|
"""Check latest self-audit summary exists and is recent."""
|
|
70
|
-
summary_file =
|
|
71
|
+
summary_file = paths.logs_dir() / "self-audit-summary.json"
|
|
71
72
|
age = _file_age_seconds(summary_file)
|
|
72
73
|
|
|
73
74
|
if age is None:
|
|
@@ -154,7 +155,7 @@ def check_schema_version() -> DoctorCheck:
|
|
|
154
155
|
"""Check DB schema version is present and reasonable."""
|
|
155
156
|
try:
|
|
156
157
|
import sqlite3
|
|
157
|
-
db_path =
|
|
158
|
+
db_path = paths.db_path()
|
|
158
159
|
if not db_path.is_file():
|
|
159
160
|
return DoctorCheck(
|
|
160
161
|
id="deep.schema_version",
|
|
@@ -187,7 +188,7 @@ def check_schema_version() -> DoctorCheck:
|
|
|
187
188
|
|
|
188
189
|
def check_preflight_summary() -> DoctorCheck:
|
|
189
190
|
"""Check runtime preflight summary."""
|
|
190
|
-
summary_file =
|
|
191
|
+
summary_file = paths.logs_dir() / "runtime-preflight-summary.json"
|
|
191
192
|
age = _file_age_seconds(summary_file)
|
|
192
193
|
|
|
193
194
|
if age is None:
|
|
@@ -242,7 +243,7 @@ def check_preflight_summary() -> DoctorCheck:
|
|
|
242
243
|
|
|
243
244
|
def check_watchdog_smoke() -> DoctorCheck:
|
|
244
245
|
"""Check watchdog smoke summary."""
|
|
245
|
-
summary_file =
|
|
246
|
+
summary_file = paths.logs_dir() / "watchdog-smoke-summary.json"
|
|
246
247
|
age = _file_age_seconds(summary_file)
|
|
247
248
|
|
|
248
249
|
if age is None:
|
|
@@ -300,7 +301,7 @@ def check_learning_count() -> DoctorCheck:
|
|
|
300
301
|
"""Check learning count as a health proxy."""
|
|
301
302
|
try:
|
|
302
303
|
import sqlite3
|
|
303
|
-
db_path =
|
|
304
|
+
db_path = paths.db_path()
|
|
304
305
|
if not db_path.is_file():
|
|
305
306
|
return DoctorCheck(
|
|
306
307
|
id="deep.learning_count",
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import datetime as dt
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import paths
|
|
7
8
|
import platform
|
|
8
9
|
import plistlib
|
|
9
10
|
import re
|
|
@@ -423,7 +424,7 @@ def _extract_declared_file_targets(args: dict, cwd: str) -> set[str]:
|
|
|
423
424
|
|
|
424
425
|
|
|
425
426
|
def _load_active_conditioned_learnings() -> list[dict]:
|
|
426
|
-
db_path =
|
|
427
|
+
db_path = paths.db_path()
|
|
427
428
|
if not db_path.is_file():
|
|
428
429
|
return []
|
|
429
430
|
try:
|
|
@@ -622,7 +623,7 @@ def _recent_codex_conditioned_file_discipline_status(*, days: int = 7, max_files
|
|
|
622
623
|
|
|
623
624
|
|
|
624
625
|
def _open_protocol_debt_summary(*debt_types: str) -> dict:
|
|
625
|
-
db_path =
|
|
626
|
+
db_path = paths.db_path()
|
|
626
627
|
summary = {"available": False, "open_total": 0, "counts": {}}
|
|
627
628
|
if not db_path.is_file() or not debt_types:
|
|
628
629
|
return summary
|
|
@@ -661,7 +662,7 @@ def _client_assumption_regressions() -> list[str]:
|
|
|
661
662
|
if not src_root.is_dir():
|
|
662
663
|
return []
|
|
663
664
|
src_root = src_root.resolve()
|
|
664
|
-
backup_root = (
|
|
665
|
+
backup_root = (paths.backups_dir()).resolve()
|
|
665
666
|
contrib_root = (NEXO_HOME / "contrib").resolve()
|
|
666
667
|
allowed_relative_paths = {
|
|
667
668
|
Path("scripts") / "deep-sleep" / "collect.py",
|
|
@@ -715,7 +716,7 @@ def _load_json(path: Path) -> dict:
|
|
|
715
716
|
|
|
716
717
|
|
|
717
718
|
def _latest_cron_run_age_seconds(cron_id: str) -> float | None:
|
|
718
|
-
db_path =
|
|
719
|
+
db_path = paths.db_path()
|
|
719
720
|
if not db_path.is_file():
|
|
720
721
|
return None
|
|
721
722
|
try:
|
|
@@ -748,7 +749,7 @@ def _latest_cron_run_age_seconds(cron_id: str) -> float | None:
|
|
|
748
749
|
def _latest_periodic_summary(kind: str) -> dict | None:
|
|
749
750
|
pattern = f"*-{kind}-summary.json"
|
|
750
751
|
candidates: list[tuple[str, Path]] = []
|
|
751
|
-
for path in (
|
|
752
|
+
for path in (paths.operations_dir() / "deep-sleep").glob(pattern):
|
|
752
753
|
try:
|
|
753
754
|
payload = json.loads(path.read_text())
|
|
754
755
|
except Exception:
|
|
@@ -830,7 +831,7 @@ def _enabled_optionals() -> dict[str, bool]:
|
|
|
830
831
|
|
|
831
832
|
def _enabled_manifest_crons() -> list[dict]:
|
|
832
833
|
manifest_candidates = [
|
|
833
|
-
|
|
834
|
+
paths.crons_dir() / "manifest.json",
|
|
834
835
|
NEXO_CODE / "crons" / "manifest.json",
|
|
835
836
|
]
|
|
836
837
|
optionals = _enabled_optionals()
|
|
@@ -1049,7 +1050,7 @@ def _plist_runtime_paths(plist_data: dict) -> list[str]:
|
|
|
1049
1050
|
|
|
1050
1051
|
|
|
1051
1052
|
def _recent_permission_denial(cron_id: str, max_age_seconds: int = 7 * 86400) -> bool:
|
|
1052
|
-
stderr_path =
|
|
1053
|
+
stderr_path = paths.logs_dir() / f"{cron_id}-stderr.log"
|
|
1053
1054
|
age = _file_age_seconds(stderr_path)
|
|
1054
1055
|
if age is None or age > max_age_seconds:
|
|
1055
1056
|
return False
|
|
@@ -1135,7 +1136,7 @@ def _sync_launchagents_from_manifest() -> tuple[bool, list[str]]:
|
|
|
1135
1136
|
|
|
1136
1137
|
def check_immune_status() -> DoctorCheck:
|
|
1137
1138
|
"""Check immune system status freshness."""
|
|
1138
|
-
status_file =
|
|
1139
|
+
status_file = paths.coordination_dir() / "immune-status.json"
|
|
1139
1140
|
age = _file_age_seconds(status_file)
|
|
1140
1141
|
|
|
1141
1142
|
if age is None:
|
|
@@ -1230,7 +1231,7 @@ def check_immune_status() -> DoctorCheck:
|
|
|
1230
1231
|
|
|
1231
1232
|
def check_watchdog_status() -> DoctorCheck:
|
|
1232
1233
|
"""Check watchdog status freshness."""
|
|
1233
|
-
status_file =
|
|
1234
|
+
status_file = paths.operations_dir() / "watchdog-status.json"
|
|
1234
1235
|
age = _file_age_seconds(status_file)
|
|
1235
1236
|
|
|
1236
1237
|
if age is None:
|
|
@@ -1307,7 +1308,7 @@ def check_stale_sessions() -> DoctorCheck:
|
|
|
1307
1308
|
"""Check for stale sessions from DB."""
|
|
1308
1309
|
try:
|
|
1309
1310
|
import sqlite3
|
|
1310
|
-
db_path =
|
|
1311
|
+
db_path = paths.db_path()
|
|
1311
1312
|
if not db_path.is_file():
|
|
1312
1313
|
return DoctorCheck(
|
|
1313
1314
|
id="runtime.stale_sessions",
|
|
@@ -1358,7 +1359,7 @@ def check_cron_freshness() -> DoctorCheck:
|
|
|
1358
1359
|
"""Check cron_runs table for recent executions."""
|
|
1359
1360
|
try:
|
|
1360
1361
|
import sqlite3
|
|
1361
|
-
db_path =
|
|
1362
|
+
db_path = paths.db_path()
|
|
1362
1363
|
if not db_path.is_file():
|
|
1363
1364
|
return DoctorCheck(
|
|
1364
1365
|
id="runtime.cron_freshness",
|
|
@@ -2404,7 +2405,7 @@ def check_protocol_compliance() -> DoctorCheck:
|
|
|
2404
2405
|
try:
|
|
2405
2406
|
import sqlite3
|
|
2406
2407
|
|
|
2407
|
-
db_path =
|
|
2408
|
+
db_path = paths.db_path()
|
|
2408
2409
|
if db_path.is_file():
|
|
2409
2410
|
conn = sqlite3.connect(str(db_path), timeout=2)
|
|
2410
2411
|
try:
|
|
@@ -2775,7 +2776,7 @@ def check_release_artifact_sync() -> DoctorCheck:
|
|
|
2775
2776
|
|
|
2776
2777
|
|
|
2777
2778
|
def check_release_trace_hygiene() -> DoctorCheck:
|
|
2778
|
-
db_path =
|
|
2779
|
+
db_path = paths.db_path()
|
|
2779
2780
|
if not db_path.is_file():
|
|
2780
2781
|
return DoctorCheck(
|
|
2781
2782
|
id="runtime.release_trace_hygiene",
|
|
@@ -2906,8 +2907,8 @@ def check_release_trace_hygiene() -> DoctorCheck:
|
|
|
2906
2907
|
|
|
2907
2908
|
|
|
2908
2909
|
def check_state_watchers() -> DoctorCheck:
|
|
2909
|
-
db_path =
|
|
2910
|
-
summary_path =
|
|
2910
|
+
db_path = paths.db_path()
|
|
2911
|
+
summary_path = paths.operations_dir() / "state-watchers-status.json"
|
|
2911
2912
|
active_watchers = 0
|
|
2912
2913
|
if db_path.is_file():
|
|
2913
2914
|
try:
|
|
@@ -3007,7 +3008,7 @@ def check_state_watchers() -> DoctorCheck:
|
|
|
3007
3008
|
|
|
3008
3009
|
|
|
3009
3010
|
def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
3010
|
-
db_path =
|
|
3011
|
+
db_path = paths.db_path()
|
|
3011
3012
|
if not db_path.is_file():
|
|
3012
3013
|
return DoctorCheck(
|
|
3013
3014
|
id="runtime.automation_telemetry",
|
package/src/evolution_cycle.py
CHANGED
|
@@ -7,6 +7,7 @@ v1.1 (future): sandbox execution of auto-approved changes.
|
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
9
|
import os
|
|
10
|
+
import paths
|
|
10
11
|
import shutil
|
|
11
12
|
import subprocess
|
|
12
13
|
import sqlite3
|
|
@@ -16,17 +17,17 @@ from pathlib import Path
|
|
|
16
17
|
|
|
17
18
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
18
19
|
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(NEXO_HOME)))
|
|
19
|
-
NEXO_DB =
|
|
20
|
+
NEXO_DB = paths.db_path()
|
|
20
21
|
SANDBOX_DIR = NEXO_HOME / "sandbox" / "workspace"
|
|
21
|
-
SNAPSHOTS_DIR =
|
|
22
|
-
RESTORE_LOG =
|
|
22
|
+
SNAPSHOTS_DIR = paths.snapshots_dir()
|
|
23
|
+
RESTORE_LOG = paths.logs_dir() / "snapshot-restores.log"
|
|
23
24
|
|
|
24
25
|
# Evolution config: brain/ (canonical) > cortex/ (legacy) > NEXO_CODE (dev)
|
|
25
26
|
def _resolve_evolution_file(name: str) -> Path:
|
|
26
|
-
for candidate in [
|
|
27
|
+
for candidate in [paths.brain_dir() / name, NEXO_HOME / "cortex" / name, NEXO_CODE / name]:
|
|
27
28
|
if candidate.exists():
|
|
28
29
|
return candidate
|
|
29
|
-
return
|
|
30
|
+
return paths.brain_dir() / name # default canonical path
|
|
30
31
|
|
|
31
32
|
OBJECTIVE_FILE = _resolve_evolution_file("evolution-objective.json")
|
|
32
33
|
PROMPT_FILE = _resolve_evolution_file("evolution-prompt.md")
|
|
@@ -233,7 +234,7 @@ def dry_run_restore_test() -> bool:
|
|
|
233
234
|
_nexo_code = Path(os.environ.get("NEXO_CODE", ""))
|
|
234
235
|
restore_script = None
|
|
235
236
|
for candidate in [_nexo_code / "scripts" / "nexo-snapshot-restore.sh",
|
|
236
|
-
|
|
237
|
+
paths.core_scripts_dir() / "nexo-snapshot-restore.sh"]:
|
|
237
238
|
if candidate.exists():
|
|
238
239
|
restore_script = candidate
|
|
239
240
|
break
|
package/src/health_check.py
CHANGED
|
@@ -47,7 +47,8 @@ def _check_runtime() -> dict:
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
def _check_database() -> dict:
|
|
50
|
-
|
|
50
|
+
import paths
|
|
51
|
+
db_path = paths.db_path()
|
|
51
52
|
out: dict[str, Any] = {"path": str(db_path), "exists": db_path.is_file()}
|
|
52
53
|
if not out["exists"]:
|
|
53
54
|
out["status"] = "error"
|
|
@@ -108,7 +109,8 @@ def _check_mcp() -> dict:
|
|
|
108
109
|
|
|
109
110
|
|
|
110
111
|
def _check_errors(hours: int = 24) -> dict:
|
|
111
|
-
|
|
112
|
+
import paths
|
|
113
|
+
ops_dir = paths.operations_dir()
|
|
112
114
|
out: dict[str, Any] = {"dir": str(ops_dir)}
|
|
113
115
|
if not ops_dir.is_dir():
|
|
114
116
|
out["recent_errors"] = 0
|