nexo-brain 5.3.0 → 5.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +10 -1
- package/bin/nexo-brain.js +59 -4
- package/bin/nexo.js +43 -7
- package/hooks/hooks.json +17 -0
- package/package.json +2 -1
- package/scripts/sync_release_artifacts.py +128 -0
- package/src/auto_update.py +70 -8
- package/src/bootstrap_docs.py +53 -2
- package/src/cli.py +3 -1
- package/src/client_sync.py +14 -1
- package/src/db/_personal_scripts.py +30 -8
- package/src/doctor/providers/runtime.py +37 -1
- package/src/hooks/heartbeat-enforcement.py +90 -0
- package/src/hooks/heartbeat-posttool.sh +18 -0
- package/src/hooks/heartbeat-user-msg.sh +15 -0
- package/src/hooks/session-start.sh +8 -4
- package/src/plugins/update.py +134 -4
- package/src/runtime_home.py +46 -0
- package/src/script_registry.py +47 -7
- package/src/scripts/nexo-cognitive-decay.py +1 -1
- package/src/scripts/nexo-cortex-cycle.py +2 -2
- package/src/scripts/nexo-hook-record.py +1 -1
- package/src/scripts/nexo-migrate.py +38 -10
- package/src/state_watchers_runtime.py +1 -1
- package/templates/CLAUDE.md.template +7 -1
- package/templates/CODEX.AGENTS.md.template +7 -1
- package/templates/launchagents/README.md +0 -1
- package/templates/nexo_helper.py +5 -1
- package/templates/launchagents/com.nexo.github-monitor.plist +0 -45
package/src/script_registry.py
CHANGED
|
@@ -16,7 +16,9 @@ import stat
|
|
|
16
16
|
import subprocess
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
from runtime_home import export_resolved_nexo_home
|
|
20
|
+
|
|
21
|
+
NEXO_HOME = export_resolved_nexo_home()
|
|
20
22
|
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
|
|
21
23
|
|
|
22
24
|
# Internal artifacts to always ignore
|
|
@@ -42,6 +44,14 @@ _LEGACY_WAKE_RECOVERY_METADATA = [
|
|
|
42
44
|
"# nexo: run_on_boot=true",
|
|
43
45
|
]
|
|
44
46
|
|
|
47
|
+
_LEGACY_CORE_RUNTIME_FILES = {
|
|
48
|
+
"capture-tool-logs.sh",
|
|
49
|
+
"daily-briefing-check.sh",
|
|
50
|
+
"heartbeat-enforcement.py",
|
|
51
|
+
"heartbeat-posttool.sh",
|
|
52
|
+
"heartbeat-user-msg.sh",
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
# Forbidden patterns — direct DB access from personal scripts
|
|
46
56
|
_FORBIDDEN_PATTERNS = [
|
|
47
57
|
re.compile(r"\bsqlite3\b"),
|
|
@@ -73,6 +83,7 @@ METADATA_KEYS = {
|
|
|
73
83
|
"run_on_wake",
|
|
74
84
|
"idempotent",
|
|
75
85
|
"max_catchup_age",
|
|
86
|
+
"doctor_allow_db",
|
|
76
87
|
}
|
|
77
88
|
SUPPORTED_RUNTIMES = {"python", "shell", "node", "php", "unknown"}
|
|
78
89
|
PERSONAL_SCHEDULE_MANAGED_ENV = "NEXO_MANAGED_PERSONAL_CRON"
|
|
@@ -116,7 +127,7 @@ def _apply_legacy_personal_script_backfills() -> None:
|
|
|
116
127
|
|
|
117
128
|
|
|
118
129
|
def load_core_script_names() -> set[str]:
|
|
119
|
-
"""Load script names
|
|
130
|
+
"""Load runtime-managed script names (core, not personal)."""
|
|
120
131
|
names: set[str] = set()
|
|
121
132
|
for manifest_path in [NEXO_CODE / "crons" / "manifest.json", NEXO_HOME / "crons" / "manifest.json"]:
|
|
122
133
|
if manifest_path.exists():
|
|
@@ -129,6 +140,26 @@ def load_core_script_names() -> set[str]:
|
|
|
129
140
|
break
|
|
130
141
|
except Exception:
|
|
131
142
|
continue
|
|
143
|
+
runtime_manifest = NEXO_HOME / "config" / "runtime-core-artifacts.json"
|
|
144
|
+
if runtime_manifest.exists():
|
|
145
|
+
try:
|
|
146
|
+
data = json.loads(runtime_manifest.read_text())
|
|
147
|
+
for key in ("script_names", "hook_names"):
|
|
148
|
+
for name in data.get(key, []):
|
|
149
|
+
clean = Path(str(name)).name
|
|
150
|
+
if clean:
|
|
151
|
+
names.add(clean)
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
hooks_dir = NEXO_HOME / "hooks"
|
|
155
|
+
if hooks_dir.is_dir():
|
|
156
|
+
try:
|
|
157
|
+
for item in hooks_dir.iterdir():
|
|
158
|
+
if item.is_file():
|
|
159
|
+
names.add(item.name)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
names.update(_LEGACY_CORE_RUNTIME_FILES)
|
|
132
163
|
return names
|
|
133
164
|
|
|
134
165
|
|
|
@@ -213,7 +244,11 @@ def _is_ignored(path: Path) -> bool:
|
|
|
213
244
|
return True
|
|
214
245
|
if path.name.startswith("."):
|
|
215
246
|
return True
|
|
216
|
-
|
|
247
|
+
try:
|
|
248
|
+
relative_path = path.resolve().relative_to(get_scripts_dir().resolve())
|
|
249
|
+
except Exception:
|
|
250
|
+
return False
|
|
251
|
+
for parent in relative_path.parents:
|
|
217
252
|
if parent.name in _IGNORED_DIRS:
|
|
218
253
|
return True
|
|
219
254
|
return False
|
|
@@ -1425,14 +1460,19 @@ def doctor_script(path_or_name: str) -> dict:
|
|
|
1425
1460
|
elif cmd:
|
|
1426
1461
|
items.append({"level": "pass", "msg": f"Required command found: {cmd}"})
|
|
1427
1462
|
|
|
1463
|
+
allow_db_access = str(meta.get("doctor_allow_db", "")).strip().lower() in {"1", "true", "yes", "on"}
|
|
1464
|
+
if allow_db_access:
|
|
1465
|
+
items.append({"level": "pass", "msg": "Doctor DB access explicitly allowed"})
|
|
1466
|
+
|
|
1428
1467
|
# Forbidden patterns (only for personal scripts)
|
|
1429
1468
|
if not is_core:
|
|
1430
1469
|
try:
|
|
1431
1470
|
content = p.read_text(errors="ignore")
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1471
|
+
if not allow_db_access:
|
|
1472
|
+
for pat in _FORBIDDEN_PATTERNS:
|
|
1473
|
+
match = pat.search(content)
|
|
1474
|
+
if match:
|
|
1475
|
+
items.append({"level": "fail", "msg": f"Forbidden DB pattern found: {match.group()}"})
|
|
1436
1476
|
except Exception:
|
|
1437
1477
|
pass
|
|
1438
1478
|
|
|
@@ -63,7 +63,7 @@ def _open_correction_fatigue_followup(fatigued: list) -> str:
|
|
|
63
63
|
lines.append(f"... and {len(fatigued) - 10} more")
|
|
64
64
|
description = "\n".join(lines)
|
|
65
65
|
verification = (
|
|
66
|
-
"sqlite3
|
|
66
|
+
"sqlite3 ~/.nexo/data/cognitive.db \"SELECT id, content, strength, tags "
|
|
67
67
|
"FROM ltm_memories WHERE tags LIKE '%under_review%' ORDER BY strength ASC LIMIT 50\""
|
|
68
68
|
)
|
|
69
69
|
now_epoch = datetime.now().timestamp()
|
|
@@ -12,7 +12,7 @@ recommendation pattern could persist indefinitely between user reports.
|
|
|
12
12
|
What this script does (idempotent and best-effort):
|
|
13
13
|
|
|
14
14
|
1. Loads cortex_evaluation_summary for the last 7 days and last 1 day.
|
|
15
|
-
2. Persists the snapshot to
|
|
15
|
+
2. Persists the snapshot to ~/.nexo/operations/cortex-quality-latest.json
|
|
16
16
|
so dashboards / morning briefings can read fresh metrics without
|
|
17
17
|
re-running the SQL.
|
|
18
18
|
3. Detects degradation signals on the 7-day window. The criteria are
|
|
@@ -25,7 +25,7 @@ What this script does (idempotent and best-effort):
|
|
|
25
25
|
metrics when degradation is detected. Idempotent: if a non-PENDING /
|
|
26
26
|
resolved followup of the same id already exists, it is updated in
|
|
27
27
|
place rather than duplicated.
|
|
28
|
-
5. Logs every run to
|
|
28
|
+
5. Logs every run to ~/.nexo/logs/cortex-cycle.log.
|
|
29
29
|
|
|
30
30
|
Catchup-friendly: a stale plist firing twice in quick succession is fine.
|
|
31
31
|
The quality file is rewritten in place, the followup is upserted, no
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Closes Fase 3 item 7 of NEXO-AUDIT-2026-04-11. Bash hooks can call:
|
|
5
5
|
|
|
6
|
-
python3
|
|
6
|
+
NEXO_HOME=~/.nexo python3 ~/.nexo/scripts/nexo-hook-record.py \
|
|
7
7
|
--hook session-start --duration-ms 142 --exit $? --session $SID
|
|
8
8
|
|
|
9
9
|
This script is intentionally minimal so it adds <50ms of latency to the
|
|
@@ -7,7 +7,7 @@ Usage:
|
|
|
7
7
|
python3 nexo-migrate.py --from 1.6.0 # override detected current version
|
|
8
8
|
|
|
9
9
|
Reads current version from $NEXO_HOME/version.json.
|
|
10
|
-
Reads target version from the
|
|
10
|
+
Reads target version from the installed runtime package.json.
|
|
11
11
|
Backs up NEXO_HOME/db/ before any migration.
|
|
12
12
|
Runs DB schema migrations via the existing _schema.py system.
|
|
13
13
|
"""
|
|
@@ -16,13 +16,43 @@ import argparse
|
|
|
16
16
|
import json
|
|
17
17
|
import os
|
|
18
18
|
import shutil
|
|
19
|
-
import sqlite3
|
|
20
19
|
import sys
|
|
21
20
|
from datetime import datetime
|
|
22
21
|
from pathlib import Path
|
|
23
22
|
|
|
24
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", Path.home() / ".nexo"))
|
|
25
|
-
|
|
23
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))).expanduser()
|
|
24
|
+
SCRIPT_ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _resolve_runtime_root() -> Path:
|
|
28
|
+
candidates: list[Path] = []
|
|
29
|
+
|
|
30
|
+
raw_code = os.environ.get("NEXO_CODE", "").strip()
|
|
31
|
+
if raw_code:
|
|
32
|
+
env_code = Path(raw_code).expanduser()
|
|
33
|
+
candidates.append(env_code)
|
|
34
|
+
if env_code.name == "src":
|
|
35
|
+
candidates.append(env_code.parent)
|
|
36
|
+
|
|
37
|
+
candidates.extend([SCRIPT_ROOT, NEXO_HOME])
|
|
38
|
+
|
|
39
|
+
seen: set[str] = set()
|
|
40
|
+
for candidate in candidates:
|
|
41
|
+
candidate = candidate if candidate.is_dir() else candidate.parent
|
|
42
|
+
key = str(candidate)
|
|
43
|
+
if key in seen:
|
|
44
|
+
continue
|
|
45
|
+
seen.add(key)
|
|
46
|
+
if (candidate / "package.json").is_file():
|
|
47
|
+
return candidate
|
|
48
|
+
if (candidate / "src" / "server.py").is_file() and (candidate / "package.json").is_file():
|
|
49
|
+
return candidate
|
|
50
|
+
|
|
51
|
+
return SCRIPT_ROOT
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
RUNTIME_ROOT = _resolve_runtime_root()
|
|
55
|
+
SOURCE_ROOT = RUNTIME_ROOT / "src" if (RUNTIME_ROOT / "src" / "db").is_dir() else RUNTIME_ROOT
|
|
26
56
|
|
|
27
57
|
|
|
28
58
|
# ── Version helpers ──────────────────────────────────────────────
|
|
@@ -56,8 +86,8 @@ def get_current_version() -> str:
|
|
|
56
86
|
|
|
57
87
|
|
|
58
88
|
def get_target_version() -> str:
|
|
59
|
-
"""Read target version from
|
|
60
|
-
pkg =
|
|
89
|
+
"""Read target version from installed runtime package.json."""
|
|
90
|
+
pkg = RUNTIME_ROOT / "package.json"
|
|
61
91
|
if not pkg.exists():
|
|
62
92
|
print(f"ERROR: package.json not found at {pkg}", file=sys.stderr)
|
|
63
93
|
sys.exit(1)
|
|
@@ -107,10 +137,8 @@ def ensure_nexo_home_dirs():
|
|
|
107
137
|
|
|
108
138
|
def run_db_schema_migrations():
|
|
109
139
|
"""Run the formal DB schema migration system from _schema.py."""
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if str(src_dir) not in sys.path:
|
|
113
|
-
sys.path.insert(0, str(src_dir))
|
|
140
|
+
if str(SOURCE_ROOT) not in sys.path:
|
|
141
|
+
sys.path.insert(0, str(SOURCE_ROOT))
|
|
114
142
|
|
|
115
143
|
# Set NEXO_HOME env for the db module
|
|
116
144
|
os.environ["NEXO_HOME"] = str(NEXO_HOME)
|
|
@@ -374,7 +374,7 @@ def _open_watcher_followup(result: dict) -> dict:
|
|
|
374
374
|
)
|
|
375
375
|
description = "\n".join(description_lines)
|
|
376
376
|
verification = (
|
|
377
|
-
f"sqlite3
|
|
377
|
+
f"sqlite3 ~/.nexo/data/nexo.db \"SELECT last_health, last_result "
|
|
378
378
|
f"FROM state_watchers WHERE watcher_id = '{watcher_id}'\""
|
|
379
379
|
)
|
|
380
380
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- nexo-claude-md-version: 2.1.
|
|
1
|
+
<!-- nexo-claude-md-version: 2.1.4 -->
|
|
2
2
|
******CORE******
|
|
3
3
|
<!-- nexo:core:start -->
|
|
4
4
|
# {{NAME}} — Cognitive Co-Operator
|
|
@@ -35,6 +35,12 @@ NEXO updates may rewrite `CORE`, but they must preserve `USER` verbatim.
|
|
|
35
35
|
- For single-artifact asks, the default cap before the first visible answer is one lookup plus one detail read. Do not keep chaining tools before answering unless the user explicitly asked for more depth.
|
|
36
36
|
- After `nexo_email_read`, `nexo_session_diary_read`, `nexo_reminders`, `nexo_followups`, or equivalent single-artifact retrieval, answer immediately. Do not launch another search, another read, or background analysis before the first user-visible answer unless the user explicitly asked for it.
|
|
37
37
|
|
|
38
|
+
## Communication Guardrail
|
|
39
|
+
- In the first answer to Francisco on any thread, lead with the direct recommendation, decision, or status.
|
|
40
|
+
- Keep each decision point to 2-3 short sentences maximum. Hold extra detail unless he explicitly asks for it.
|
|
41
|
+
- Do not use internal NEXO jargon in the first answer (`protocol debt`, `cortex evaluation`, `runtime check`, `guard_check`, `heartbeat`, etc.). Translate it into plain operational language.
|
|
42
|
+
- Prefer conclusion plus next action over option dumps, raw diagnostics, or internal process narration. Apply this equally to chat replies, emails, briefings, and headless reports intended for Francisco.
|
|
43
|
+
|
|
38
44
|
<!-- nexo:start:profile -->
|
|
39
45
|
## User Profile
|
|
40
46
|
- **Calibration:** `{{NEXO_HOME}}/brain/calibration.json` (personality settings + language + user name)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- nexo-codex-agents-version: 1.2.
|
|
1
|
+
<!-- nexo-codex-agents-version: 1.2.4 -->
|
|
2
2
|
******CORE******
|
|
3
3
|
<!-- nexo:core:start -->
|
|
4
4
|
# {{NAME}} — NEXO Shared Brain for Codex
|
|
@@ -32,6 +32,12 @@ NEXO updates may rewrite `CORE`, but they must preserve `USER` verbatim.
|
|
|
32
32
|
- For single-artifact asks, the default cap before the first visible answer is one lookup plus one detail read. Do not keep chaining tools before answering unless the user explicitly asked for more depth.
|
|
33
33
|
- After `nexo_email_read`, `nexo_session_diary_read`, `nexo_reminders`, `nexo_followups`, or equivalent single-artifact retrieval, answer immediately. Do not launch another search, another read, or background analysis before the first visible answer unless the user explicitly asked for it.
|
|
34
34
|
|
|
35
|
+
## Communication Guardrail
|
|
36
|
+
- In the first answer to Francisco on any thread, lead with the direct recommendation, decision, or status.
|
|
37
|
+
- Keep each decision point to 2-3 short sentences maximum. Hold extra detail unless he explicitly asks for it.
|
|
38
|
+
- Do not use internal NEXO jargon in the first answer (`protocol debt`, `cortex evaluation`, `runtime check`, `guard_check`, `heartbeat`, etc.). Translate it into plain operational language.
|
|
39
|
+
- Prefer conclusion plus next action over option dumps, raw diagnostics, or internal process narration. Apply this equally to chat replies, emails, briefings, and headless reports intended for Francisco.
|
|
40
|
+
|
|
35
41
|
## Codex Runtime Notes
|
|
36
42
|
- Codex does not provide Claude Code hooks, so protocol discipline must be explicit.
|
|
37
43
|
- If a stable session token is useful, pass `session_token='codex-<task>-<date>'` and `session_client='codex'` to `nexo_startup`; otherwise leave them blank.
|
|
@@ -112,7 +112,6 @@ These agents power NEXO's learning and memory systems. Strongly recommended.
|
|
|
112
112
|
| File | Schedule | What it does |
|
|
113
113
|
|------|----------|-------------|
|
|
114
114
|
| `com.nexo.dashboard.plist` | Persistent (KeepAlive) | Runs the NEXO web dashboard on `http://localhost:6174`. Provides a browser-based view of sessions, reminders, followups, and system health. Only needed if you want the dashboard UI. |
|
|
115
|
-
| `com.nexo.github-monitor.plist` | Daily 08:00 | Checks the NEXO public GitHub repository for open issues, pull requests, and pending releases. Writes results to `~/.nexo/github-status.json` for NEXO to read at startup. Only relevant if you maintain the public NEXO repository. |
|
|
116
115
|
|
|
117
116
|
---
|
|
118
117
|
|
package/templates/nexo_helper.py
CHANGED
|
@@ -30,11 +30,15 @@ def _detect_nexo_home() -> Path:
|
|
|
30
30
|
):
|
|
31
31
|
return inferred_home
|
|
32
32
|
|
|
33
|
+
default_home = Path.home() / ".nexo"
|
|
34
|
+
if default_home.is_dir():
|
|
35
|
+
return default_home
|
|
36
|
+
|
|
33
37
|
claude_home = Path.home() / "claude"
|
|
34
38
|
if claude_home.is_dir():
|
|
35
39
|
return claude_home
|
|
36
40
|
|
|
37
|
-
return
|
|
41
|
+
return default_home
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
NEXO_HOME = _detect_nexo_home()
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!-- com.nexo.github-monitor
|
|
3
|
-
Runs nexo-github-monitor.py every day at 08:00 to check the NEXO
|
|
4
|
-
public GitHub repository for new issues, pull requests, and pending
|
|
5
|
-
releases. Writes results to ~/.nexo/github-status.json. At the next
|
|
6
|
-
session startup, NEXO reads this file and responds to open issues,
|
|
7
|
-
reviews PRs, and proposes a release if enough commits have
|
|
8
|
-
accumulated since the last tag.
|
|
9
|
-
-->
|
|
10
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
11
|
-
<plist version="1.0">
|
|
12
|
-
<dict>
|
|
13
|
-
<key>Label</key>
|
|
14
|
-
<string>com.nexo.github-monitor</string>
|
|
15
|
-
<key>ProgramArguments</key>
|
|
16
|
-
<array>
|
|
17
|
-
<string>/usr/bin/python3</string>
|
|
18
|
-
<string>{{NEXO_HOME}}/scripts/nexo-github-monitor.py</string>
|
|
19
|
-
</array>
|
|
20
|
-
<key>StartCalendarInterval</key>
|
|
21
|
-
<dict>
|
|
22
|
-
<key>Hour</key>
|
|
23
|
-
<integer>8</integer>
|
|
24
|
-
<key>Minute</key>
|
|
25
|
-
<integer>0</integer>
|
|
26
|
-
</dict>
|
|
27
|
-
<key>StandardOutPath</key>
|
|
28
|
-
<string>{{NEXO_HOME}}/logs/github-monitor-stdout.log</string>
|
|
29
|
-
<key>StandardErrorPath</key>
|
|
30
|
-
<string>{{NEXO_HOME}}/logs/github-monitor-stderr.log</string>
|
|
31
|
-
<key>EnvironmentVariables</key>
|
|
32
|
-
<dict>
|
|
33
|
-
<key>HOME</key>
|
|
34
|
-
<string>{{HOME}}</string>
|
|
35
|
-
<key>NEXO_HOME</key>
|
|
36
|
-
<string>{{NEXO_HOME}}</string>
|
|
37
|
-
<key>NEXO_CODE</key>
|
|
38
|
-
<string>{{NEXO_CODE}}</string>
|
|
39
|
-
<key>PATH</key>
|
|
40
|
-
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
41
|
-
</dict>
|
|
42
|
-
<key>RunAtLoad</key>
|
|
43
|
-
<false/>
|
|
44
|
-
</dict>
|
|
45
|
-
</plist>
|