nexo-brain 5.3.0 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +8 -1
- package/bin/nexo-brain.js +30 -4
- package/bin/nexo.js +43 -7
- package/package.json +2 -1
- package/scripts/sync_release_artifacts.py +128 -0
- package/src/auto_update.py +66 -8
- package/src/bootstrap_docs.py +53 -2
- package/src/cli.py +3 -1
- package/src/client_sync.py +2 -1
- package/src/db/_personal_scripts.py +30 -8
- package/src/doctor/providers/runtime.py +37 -1
- package/src/hooks/session-start.sh +8 -4
- package/src/plugins/update.py +90 -3
- package/src/runtime_home.py +46 -0
- package/src/script_registry.py +18 -6
- 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/nexo_helper.py +5 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -87,9 +87,16 @@ Versions `3.1.7` through `3.2.0` close the recent-memory gap:
|
|
|
87
87
|
- when even that misses, NEXO now exposes raw transcript fallback tools for Claude Code and Codex session stores
|
|
88
88
|
- NEXO can now inspect itself through a live system catalog derived from canonical sources instead of relying only on stale docs or operator memory
|
|
89
89
|
|
|
90
|
+
Version `5.3.1` normalizes packaged npm installs so they behave like packaged npm installs: `nexo update` now keeps the runtime anchored to `~/.nexo`, refreshes packaged bootstrap/client artifacts after upgrade, avoids repo-only release-artifact drift in installed runtimes, and keeps personal scripts on the canonical packaged path.
|
|
91
|
+
|
|
90
92
|
Version `5.3.0` adds `nexo uninstall` — a CLI command that cleanly separates runtime from user data. It stops all crons, removes the MCP server config, and preserves databases, learnings, and personal scripts for safe reinstall.
|
|
91
93
|
|
|
92
|
-
Version `5.2.1`
|
|
94
|
+
Version `5.2.1` fixes the Deep Sleep datetime regression and closes the decision-to-outcome feedback gap:
|
|
95
|
+
|
|
96
|
+
- `_parse_any_datetime` in `apply_findings.py` now strips timezone info before comparison, fixing the offset-aware/offset-naive crash that was breaking Deep Sleep verification work.
|
|
97
|
+
- `cortex_decide()` now auto-creates a `decision_outcome` when none is linked yet, so the outcome-checker cron can verify real decisions instead of leaving the loop open.
|
|
98
|
+
|
|
99
|
+
Version `5.2.0` closes two focused gaps in the Cortex layer that were left open by the v5.1 audit — the high-stakes response-contract detector was English-only, and the `nexo-cortex-cycle` cron was writing a quality snapshot that no reader ever consumed:
|
|
93
100
|
|
|
94
101
|
- `HIGH_STAKES_KEYWORDS_ES` adds ~45 Spanish keywords to the high-stakes detector with accented and unaccented variants, so a goal written in Spanish (`migrar la base de datos de producción`) trips the same gate as its English twin.
|
|
95
102
|
- `NEGATION_PATTERNS` suppresses false positives when the user explicitly disclaims touching the sensitive area (`sin afectar producción`, `no tocar prod`, `without touching production`, `don't modify`). The raw keyword being present is no longer enough to flag the task.
|
package/bin/nexo-brain.js
CHANGED
|
@@ -2210,15 +2210,31 @@ async function main() {
|
|
|
2210
2210
|
"#!/usr/bin/env bash",
|
|
2211
2211
|
"set -euo pipefail",
|
|
2212
2212
|
"",
|
|
2213
|
-
`
|
|
2213
|
+
`RUNTIME_HOME="${NEXO_HOME}"`,
|
|
2214
|
+
'NEXO_HOME="$RUNTIME_HOME"',
|
|
2214
2215
|
'export NEXO_HOME',
|
|
2215
|
-
'
|
|
2216
|
+
'resolve_code_dir() {',
|
|
2217
|
+
' if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/cli.py" ]; then',
|
|
2218
|
+
' printf \'%s\\n\' "${NEXO_CODE%/}"',
|
|
2219
|
+
' return 0',
|
|
2220
|
+
' fi',
|
|
2221
|
+
' if [ -f "$NEXO_HOME/cli.py" ]; then',
|
|
2222
|
+
' printf \'%s\\n\' "$NEXO_HOME"',
|
|
2223
|
+
' return 0',
|
|
2224
|
+
' fi',
|
|
2225
|
+
' printf \'%s\\n\' "$NEXO_HOME"',
|
|
2226
|
+
'}',
|
|
2227
|
+
'NEXO_CODE="$(resolve_code_dir)"',
|
|
2228
|
+
'export NEXO_CODE',
|
|
2216
2229
|
'resolve_python() {',
|
|
2217
2230
|
' local candidates=()',
|
|
2218
2231
|
' local candidate=""',
|
|
2219
2232
|
' if [ -n "${NEXO_RUNTIME_PYTHON:-}" ]; then candidates+=("$NEXO_RUNTIME_PYTHON"); fi',
|
|
2220
2233
|
' if [ -n "${NEXO_PYTHON:-}" ]; then candidates+=("$NEXO_PYTHON"); fi',
|
|
2221
|
-
' candidates+=("$
|
|
2234
|
+
' candidates+=("$NEXO_CODE/.venv/bin/python3" "$NEXO_CODE/.venv/bin/python")',
|
|
2235
|
+
' if [ "$NEXO_CODE" != "$NEXO_HOME" ]; then',
|
|
2236
|
+
' candidates+=("$NEXO_HOME/.venv/bin/python3" "$NEXO_HOME/.venv/bin/python")',
|
|
2237
|
+
' fi',
|
|
2222
2238
|
' case "$(uname -s)" in',
|
|
2223
2239
|
' Darwin) candidates+=("/opt/homebrew/bin/python3" "/usr/local/bin/python3") ;;',
|
|
2224
2240
|
' *) candidates+=("/usr/local/bin/python3" "/usr/bin/python3") ;;',
|
|
@@ -2246,7 +2262,17 @@ async function main() {
|
|
|
2246
2262
|
' echo "NEXO runtime Python not found. Run nexo-brain or nexo update to repair the installation." >&2',
|
|
2247
2263
|
' exit 1',
|
|
2248
2264
|
'fi',
|
|
2249
|
-
'
|
|
2265
|
+
'CLI_PY="$NEXO_CODE/cli.py"',
|
|
2266
|
+
'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/cli.py" ]; then',
|
|
2267
|
+
' NEXO_CODE="$NEXO_HOME"',
|
|
2268
|
+
' export NEXO_CODE',
|
|
2269
|
+
' CLI_PY="$NEXO_HOME/cli.py"',
|
|
2270
|
+
'fi',
|
|
2271
|
+
'if [ ! -f "$CLI_PY" ]; then',
|
|
2272
|
+
' echo "NEXO CLI not found under $NEXO_HOME. Run nexo-brain or nexo update to repair the installation." >&2',
|
|
2273
|
+
' exit 1',
|
|
2274
|
+
'fi',
|
|
2275
|
+
'exec "$PYTHON" "$CLI_PY" "$@"',
|
|
2250
2276
|
"",
|
|
2251
2277
|
].join("\n");
|
|
2252
2278
|
const runtimeCliPath = path.join(NEXO_HOME, "bin", "nexo");
|
package/bin/nexo.js
CHANGED
|
@@ -12,7 +12,44 @@ const fs = require("fs");
|
|
|
12
12
|
const os = require("os");
|
|
13
13
|
const path = require("path");
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
function resolveNexoHome(rawValue) {
|
|
16
|
+
const homeDir = os.homedir();
|
|
17
|
+
const managedHome = path.join(homeDir, ".nexo");
|
|
18
|
+
const legacyHome = path.join(homeDir, "claude");
|
|
19
|
+
const candidate = rawValue || managedHome;
|
|
20
|
+
|
|
21
|
+
if (candidate === managedHome) return managedHome;
|
|
22
|
+
if (candidate === legacyHome) return fs.existsSync(managedHome) ? managedHome : candidate;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
if (fs.existsSync(managedHome) && fs.realpathSync.native(candidate) === fs.realpathSync.native(managedHome)) {
|
|
26
|
+
return managedHome;
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const NEXO_HOME = resolveNexoHome(process.env.NEXO_HOME);
|
|
34
|
+
const NEXO_CODE = resolveCodeDir();
|
|
35
|
+
|
|
36
|
+
function resolveCodeDir() {
|
|
37
|
+
const envCode = process.env.NEXO_CODE;
|
|
38
|
+
if (envCode && fs.existsSync(path.join(envCode, "cli.py"))) {
|
|
39
|
+
return envCode;
|
|
40
|
+
}
|
|
41
|
+
const repoCandidate = path.join(__dirname, "..", "src", "cli.py");
|
|
42
|
+
if (fs.existsSync(repoCandidate)) {
|
|
43
|
+
return path.join(__dirname, "..", "src");
|
|
44
|
+
}
|
|
45
|
+
if (fs.existsSync(path.join(NEXO_HOME, "cli.py"))) {
|
|
46
|
+
return NEXO_HOME;
|
|
47
|
+
}
|
|
48
|
+
if (fs.existsSync(path.join(NEXO_HOME, "claude", "cli.py"))) {
|
|
49
|
+
return path.join(NEXO_HOME, "claude");
|
|
50
|
+
}
|
|
51
|
+
return NEXO_HOME;
|
|
52
|
+
}
|
|
16
53
|
|
|
17
54
|
function pythonSupportsModule(candidate, moduleName) {
|
|
18
55
|
if (!candidate) return false;
|
|
@@ -23,7 +60,7 @@ function pythonSupportsModule(candidate, moduleName) {
|
|
|
23
60
|
env: {
|
|
24
61
|
...process.env,
|
|
25
62
|
NEXO_HOME,
|
|
26
|
-
NEXO_CODE
|
|
63
|
+
NEXO_CODE,
|
|
27
64
|
},
|
|
28
65
|
});
|
|
29
66
|
return result.status === 0;
|
|
@@ -36,6 +73,8 @@ function findPython() {
|
|
|
36
73
|
const candidates = [
|
|
37
74
|
process.env.NEXO_RUNTIME_PYTHON,
|
|
38
75
|
process.env.NEXO_PYTHON,
|
|
76
|
+
path.join(NEXO_CODE, ".venv", "bin", "python3"),
|
|
77
|
+
path.join(NEXO_CODE, ".venv", "bin", "python"),
|
|
39
78
|
path.join(NEXO_HOME, ".venv", "bin", "python3"),
|
|
40
79
|
path.join(NEXO_HOME, ".venv", "bin", "python"),
|
|
41
80
|
process.platform === "darwin" ? "/opt/homebrew/bin/python3" : "",
|
|
@@ -55,10 +94,7 @@ function findPython() {
|
|
|
55
94
|
}
|
|
56
95
|
|
|
57
96
|
function findCliPy() {
|
|
58
|
-
|
|
59
|
-
const installedCandidate = path.join(NEXO_HOME, "cli.py");
|
|
60
|
-
if (fs.existsSync(repoCandidate)) return repoCandidate;
|
|
61
|
-
return installedCandidate;
|
|
97
|
+
return path.join(NEXO_CODE, "cli.py");
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
const python = findPython();
|
|
@@ -75,7 +111,7 @@ const result = spawnSync(python, [cliPy, ...process.argv.slice(2)], {
|
|
|
75
111
|
env: {
|
|
76
112
|
...process.env,
|
|
77
113
|
NEXO_HOME,
|
|
78
|
-
NEXO_CODE
|
|
114
|
+
NEXO_CODE,
|
|
79
115
|
},
|
|
80
116
|
});
|
|
81
117
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.1",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"bin/nexo-brain.js",
|
|
68
68
|
"bin/nexo.js",
|
|
69
69
|
"bin/postinstall.js",
|
|
70
|
+
"scripts/sync_release_artifacts.py",
|
|
70
71
|
"src/",
|
|
71
72
|
"community/",
|
|
72
73
|
"!src/**/__pycache__",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
11
|
+
ROOT_PACKAGE_JSON = ROOT / "package.json"
|
|
12
|
+
CLAUDE_PLUGIN_JSON = ROOT / ".claude-plugin" / "plugin.json"
|
|
13
|
+
CLAWHUB_SKILL_MD = ROOT / "clawhub-skill" / "SKILL.md"
|
|
14
|
+
OPENCLAW_PACKAGE_JSON = ROOT / "openclaw-plugin" / "package.json"
|
|
15
|
+
OPENCLAW_MCP_BRIDGE = ROOT / "openclaw-plugin" / "src" / "mcp-bridge.ts"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def fail(message: str) -> None:
|
|
19
|
+
raise SystemExit(f"[sync-release-artifacts] {message}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_json(path: Path) -> dict:
|
|
23
|
+
return json.loads(path.read_text())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def dump_json(path: Path, payload: dict) -> None:
|
|
27
|
+
path.write_text(json.dumps(payload, indent=2) + "\n")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sync_json_version(path: Path, expected_version: str, label: str) -> bool:
|
|
31
|
+
payload = load_json(path)
|
|
32
|
+
if payload.get("version") == expected_version:
|
|
33
|
+
return False
|
|
34
|
+
payload["version"] = expected_version
|
|
35
|
+
dump_json(path, payload)
|
|
36
|
+
print(f"[sync-release-artifacts] synced {label} version -> {expected_version}")
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sync_clawhub_skill(skill_path: Path, expected_version: str) -> bool:
|
|
41
|
+
text = skill_path.read_text()
|
|
42
|
+
updated = text
|
|
43
|
+
|
|
44
|
+
updated = re.sub(
|
|
45
|
+
r"(?m)^version:\s*[^\n]+$",
|
|
46
|
+
f"version: {expected_version}",
|
|
47
|
+
updated,
|
|
48
|
+
count=1,
|
|
49
|
+
)
|
|
50
|
+
updated = updated.replace("~/.nexo/src/server.py", "~/.nexo/server.py")
|
|
51
|
+
|
|
52
|
+
if updated == text:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
skill_path.write_text(updated)
|
|
56
|
+
print(f"[sync-release-artifacts] synced ClawHub skill -> {expected_version}")
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def sync_openclaw_bridge(bridge_path: Path, expected_version: str) -> bool:
|
|
61
|
+
text = bridge_path.read_text()
|
|
62
|
+
updated = text
|
|
63
|
+
|
|
64
|
+
updated = updated.replace(
|
|
65
|
+
'resolve(this.config.nexoHome, "src", "server.py")',
|
|
66
|
+
'resolve(this.config.nexoHome, "server.py")',
|
|
67
|
+
)
|
|
68
|
+
updated = re.sub(
|
|
69
|
+
r'clientInfo:\s*\{\s*name:\s*"openclaw-memory-nexo-brain",\s*version:\s*"[^"]+"\s*\}',
|
|
70
|
+
f'clientInfo: {{ name: "openclaw-memory-nexo-brain", version: "{expected_version}" }}',
|
|
71
|
+
updated,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if updated == text:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
bridge_path.write_text(updated)
|
|
78
|
+
print(f"[sync-release-artifacts] synced OpenClaw bridge -> {expected_version}")
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main() -> None:
|
|
83
|
+
parser = argparse.ArgumentParser(description="Keep release-facing integration artifacts in sync.")
|
|
84
|
+
parser.add_argument("--release-version", help="Expected release version (must match root package.json).")
|
|
85
|
+
parser.add_argument("--check", action="store_true", help="Fail if any artifact would change.")
|
|
86
|
+
args = parser.parse_args()
|
|
87
|
+
|
|
88
|
+
root_package = load_json(ROOT_PACKAGE_JSON)
|
|
89
|
+
root_version = root_package.get("version")
|
|
90
|
+
if not root_version:
|
|
91
|
+
fail("root package.json is missing version")
|
|
92
|
+
|
|
93
|
+
if args.release_version and args.release_version != root_version:
|
|
94
|
+
fail(
|
|
95
|
+
f"release version {args.release_version} does not match root package.json version {root_version}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
original_payloads = {
|
|
99
|
+
CLAUDE_PLUGIN_JSON: CLAUDE_PLUGIN_JSON.read_text(),
|
|
100
|
+
CLAWHUB_SKILL_MD: CLAWHUB_SKILL_MD.read_text(),
|
|
101
|
+
OPENCLAW_PACKAGE_JSON: OPENCLAW_PACKAGE_JSON.read_text(),
|
|
102
|
+
OPENCLAW_MCP_BRIDGE: OPENCLAW_MCP_BRIDGE.read_text(),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
changed = []
|
|
106
|
+
if sync_json_version(CLAUDE_PLUGIN_JSON, root_version, "Claude plugin"):
|
|
107
|
+
changed.append(".claude-plugin/plugin.json")
|
|
108
|
+
if sync_clawhub_skill(CLAWHUB_SKILL_MD, root_version):
|
|
109
|
+
changed.append("clawhub-skill/SKILL.md")
|
|
110
|
+
if sync_json_version(OPENCLAW_PACKAGE_JSON, root_version, "OpenClaw package"):
|
|
111
|
+
changed.append("openclaw-plugin/package.json")
|
|
112
|
+
if sync_openclaw_bridge(OPENCLAW_MCP_BRIDGE, root_version):
|
|
113
|
+
changed.append("openclaw-plugin/src/mcp-bridge.ts")
|
|
114
|
+
|
|
115
|
+
if args.check:
|
|
116
|
+
for path, text in original_payloads.items():
|
|
117
|
+
path.write_text(text)
|
|
118
|
+
if changed:
|
|
119
|
+
fail("artifacts out of sync: " + ", ".join(changed))
|
|
120
|
+
print("[sync-release-artifacts] OK")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
if not changed:
|
|
124
|
+
print("[sync-release-artifacts] already in sync")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
package/src/auto_update.py
CHANGED
|
@@ -17,7 +17,9 @@ import sys
|
|
|
17
17
|
import time
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
from runtime_home import export_resolved_nexo_home, managed_nexo_home
|
|
21
|
+
|
|
22
|
+
NEXO_HOME = export_resolved_nexo_home()
|
|
21
23
|
DATA_DIR = NEXO_HOME / "data"
|
|
22
24
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
23
25
|
|
|
@@ -148,15 +150,42 @@ def _runtime_cli_wrapper_text() -> str:
|
|
|
148
150
|
return (
|
|
149
151
|
"#!/usr/bin/env bash\n"
|
|
150
152
|
"set -euo pipefail\n\n"
|
|
151
|
-
f'
|
|
153
|
+
f'DEFAULT_NEXO_HOME="{managed_nexo_home()}"\n'
|
|
154
|
+
'RUNTIME_HOME="${NEXO_HOME:-$DEFAULT_NEXO_HOME}"\n'
|
|
155
|
+
'if [ "$RUNTIME_HOME" = "${HOME}/claude" ] && [ -e "$DEFAULT_NEXO_HOME" ]; then\n'
|
|
156
|
+
' RUNTIME_HOME="$DEFAULT_NEXO_HOME"\n'
|
|
157
|
+
'fi\n'
|
|
158
|
+
'if [ -e "$RUNTIME_HOME" ] && [ -e "$DEFAULT_NEXO_HOME" ]; then\n'
|
|
159
|
+
' RESOLVED_RUNTIME="$(cd "$RUNTIME_HOME" 2>/dev/null && pwd -P || true)"\n'
|
|
160
|
+
' RESOLVED_DEFAULT="$(cd "$DEFAULT_NEXO_HOME" 2>/dev/null && pwd -P || true)"\n'
|
|
161
|
+
' if [ -n "$RESOLVED_RUNTIME" ] && [ "$RESOLVED_RUNTIME" = "$RESOLVED_DEFAULT" ]; then\n'
|
|
162
|
+
' RUNTIME_HOME="$DEFAULT_NEXO_HOME"\n'
|
|
163
|
+
' fi\n'
|
|
164
|
+
'fi\n'
|
|
165
|
+
'NEXO_HOME="$RUNTIME_HOME"\n'
|
|
152
166
|
'export NEXO_HOME\n'
|
|
153
|
-
'
|
|
167
|
+
'resolve_code_dir() {\n'
|
|
168
|
+
' if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/cli.py" ]; then\n'
|
|
169
|
+
' printf \'%s\\n\' "${NEXO_CODE%/}"\n'
|
|
170
|
+
' return 0\n'
|
|
171
|
+
' fi\n'
|
|
172
|
+
' if [ -f "$NEXO_HOME/cli.py" ]; then\n'
|
|
173
|
+
' printf \'%s\\n\' "$NEXO_HOME"\n'
|
|
174
|
+
' return 0\n'
|
|
175
|
+
' fi\n'
|
|
176
|
+
' printf \'%s\\n\' "$NEXO_HOME"\n'
|
|
177
|
+
'}\n'
|
|
178
|
+
'NEXO_CODE="$(resolve_code_dir)"\n'
|
|
179
|
+
'export NEXO_CODE\n'
|
|
154
180
|
'resolve_python() {\n'
|
|
155
181
|
' local candidates=()\n'
|
|
156
182
|
' local candidate=""\n'
|
|
157
183
|
' if [ -n "${NEXO_RUNTIME_PYTHON:-}" ]; then candidates+=("$NEXO_RUNTIME_PYTHON"); fi\n'
|
|
158
184
|
' if [ -n "${NEXO_PYTHON:-}" ]; then candidates+=("$NEXO_PYTHON"); fi\n'
|
|
159
|
-
' candidates+=("$
|
|
185
|
+
' candidates+=("$NEXO_CODE/.venv/bin/python3" "$NEXO_CODE/.venv/bin/python")\n'
|
|
186
|
+
' if [ "$NEXO_CODE" != "$NEXO_HOME" ]; then\n'
|
|
187
|
+
' candidates+=("$NEXO_HOME/.venv/bin/python3" "$NEXO_HOME/.venv/bin/python")\n'
|
|
188
|
+
' fi\n'
|
|
160
189
|
' case "$(uname -s)" in\n'
|
|
161
190
|
' Darwin) candidates+=("/opt/homebrew/bin/python3" "/usr/local/bin/python3") ;;\n'
|
|
162
191
|
' *) candidates+=("/usr/local/bin/python3" "/usr/bin/python3") ;;\n'
|
|
@@ -184,7 +213,17 @@ def _runtime_cli_wrapper_text() -> str:
|
|
|
184
213
|
' echo "NEXO runtime Python not found. Run nexo-brain or nexo update to repair the installation." >&2\n'
|
|
185
214
|
' exit 1\n'
|
|
186
215
|
'fi\n'
|
|
187
|
-
'
|
|
216
|
+
'CLI_PY="$NEXO_CODE/cli.py"\n'
|
|
217
|
+
'if [ ! -f "$CLI_PY" ] && [ -f "$NEXO_HOME/cli.py" ]; then\n'
|
|
218
|
+
' NEXO_CODE="$NEXO_HOME"\n'
|
|
219
|
+
' export NEXO_CODE\n'
|
|
220
|
+
' CLI_PY="$NEXO_HOME/cli.py"\n'
|
|
221
|
+
'fi\n'
|
|
222
|
+
'if [ ! -f "$CLI_PY" ]; then\n'
|
|
223
|
+
' echo "NEXO CLI not found under $NEXO_HOME. Run nexo-brain or nexo update to repair the installation." >&2\n'
|
|
224
|
+
' exit 1\n'
|
|
225
|
+
'fi\n'
|
|
226
|
+
'exec "$PYTHON" "$CLI_PY" "$@"\n'
|
|
188
227
|
)
|
|
189
228
|
|
|
190
229
|
|
|
@@ -1451,9 +1490,11 @@ def _resolve_sync_source() -> tuple[Path | None, Path | None]:
|
|
|
1451
1490
|
if (
|
|
1452
1491
|
not same_as_runtime
|
|
1453
1492
|
and (NEXO_CODE / "db").is_dir()
|
|
1454
|
-
and (NEXO_CODE.parent / "package.json").is_file()
|
|
1455
1493
|
):
|
|
1456
|
-
|
|
1494
|
+
if (NEXO_CODE.parent / "package.json").is_file():
|
|
1495
|
+
return NEXO_CODE, NEXO_CODE.parent
|
|
1496
|
+
if (NEXO_CODE / "package.json").is_file():
|
|
1497
|
+
return NEXO_CODE, NEXO_CODE
|
|
1457
1498
|
|
|
1458
1499
|
version_source = _runtime_version_source()
|
|
1459
1500
|
if version_source:
|
|
@@ -1583,7 +1624,21 @@ def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
|
|
|
1583
1624
|
backup_dir = NEXO_HOME / "backups" / f"runtime-tree-{timestamp}"
|
|
1584
1625
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
1585
1626
|
|
|
1586
|
-
code_dirs = [
|
|
1627
|
+
code_dirs = [
|
|
1628
|
+
"hooks",
|
|
1629
|
+
"plugins",
|
|
1630
|
+
"db",
|
|
1631
|
+
"cognitive",
|
|
1632
|
+
"dashboard",
|
|
1633
|
+
"rules",
|
|
1634
|
+
"crons",
|
|
1635
|
+
"scripts",
|
|
1636
|
+
"doctor",
|
|
1637
|
+
"skills",
|
|
1638
|
+
"skills-core",
|
|
1639
|
+
"skills-runtime",
|
|
1640
|
+
"templates",
|
|
1641
|
+
]
|
|
1587
1642
|
flat_files = _runtime_flat_files(dest)
|
|
1588
1643
|
for name in code_dirs:
|
|
1589
1644
|
src = dest / name
|
|
@@ -1636,6 +1691,9 @@ def _copy_runtime_from_source(src_dir: Path, repo_dir: Path, dest: Path = NEXO_H
|
|
|
1636
1691
|
script_conflicts: list[dict[str, str]] = []
|
|
1637
1692
|
installed_script_classes = _installed_scripts_classification(dest)
|
|
1638
1693
|
|
|
1694
|
+
for dirname in ("bin", "skills", "skills-core", "skills-runtime", "templates"):
|
|
1695
|
+
(dest / dirname).mkdir(parents=True, exist_ok=True)
|
|
1696
|
+
|
|
1639
1697
|
_emit_progress(progress_fn, "Copying core packages...")
|
|
1640
1698
|
for pkg in packages:
|
|
1641
1699
|
pkg_src = src_dir / pkg
|
package/src/bootstrap_docs.py
CHANGED
|
@@ -16,6 +16,7 @@ from client_preferences import (
|
|
|
16
16
|
normalize_client_key,
|
|
17
17
|
normalize_client_preferences,
|
|
18
18
|
)
|
|
19
|
+
from runtime_home import resolve_nexo_home
|
|
19
20
|
|
|
20
21
|
def _resolve_templates_dir(module_file: str | os.PathLike[str]) -> Path:
|
|
21
22
|
module_dir = Path(module_file).resolve().parent
|
|
@@ -60,7 +61,7 @@ def _user_home() -> Path:
|
|
|
60
61
|
|
|
61
62
|
|
|
62
63
|
def _default_nexo_home() -> Path:
|
|
63
|
-
return
|
|
64
|
+
return resolve_nexo_home(os.environ.get("NEXO_HOME", str(_user_home() / ".nexo")))
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
|
|
@@ -115,6 +116,18 @@ def _build_user_block(user_payload: str, template_text: str) -> str:
|
|
|
115
116
|
return f"{USER_START}\n{user_payload.rstrip()}\n{USER_END}"
|
|
116
117
|
|
|
117
118
|
|
|
119
|
+
def _extract_user_payload(text: str) -> str:
|
|
120
|
+
block = _extract_block(text, USER_START, USER_END)
|
|
121
|
+
if not block:
|
|
122
|
+
return ""
|
|
123
|
+
payload = block
|
|
124
|
+
if payload.startswith(USER_START):
|
|
125
|
+
payload = payload[len(USER_START):]
|
|
126
|
+
if payload.endswith(USER_END):
|
|
127
|
+
payload = payload[: -len(USER_END)]
|
|
128
|
+
return payload.strip("\n")
|
|
129
|
+
|
|
130
|
+
|
|
118
131
|
def _legacy_user_payload(existing_text: str) -> str:
|
|
119
132
|
cleaned = re.sub(r"<!--\s*nexo-[^-]+-[^-]+-version:\s*[\d.]+\s*-->\s*", "", existing_text)
|
|
120
133
|
if "<!-- nexo:start:" in cleaned:
|
|
@@ -146,6 +159,33 @@ def _legacy_user_payload(existing_text: str) -> str:
|
|
|
146
159
|
return cleaned.strip()
|
|
147
160
|
|
|
148
161
|
|
|
162
|
+
def _home_alias(path: Path, user_home: Path) -> str:
|
|
163
|
+
try:
|
|
164
|
+
relative = path.expanduser().relative_to(user_home.expanduser())
|
|
165
|
+
except Exception:
|
|
166
|
+
return str(path.expanduser())
|
|
167
|
+
return "~/" + relative.as_posix()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _migrate_legacy_nexo_home_references(user_payload: str, *, nexo_home: Path, user_home: Path) -> str:
|
|
171
|
+
if not user_payload.strip():
|
|
172
|
+
return user_payload
|
|
173
|
+
legacy_home = user_home.expanduser() / "claude"
|
|
174
|
+
target_home = nexo_home.expanduser()
|
|
175
|
+
if legacy_home == target_home:
|
|
176
|
+
return user_payload
|
|
177
|
+
|
|
178
|
+
replacements = [
|
|
179
|
+
(str(legacy_home), str(target_home)),
|
|
180
|
+
(_home_alias(legacy_home, user_home), _home_alias(target_home, user_home)),
|
|
181
|
+
]
|
|
182
|
+
updated = user_payload
|
|
183
|
+
for old, new in replacements:
|
|
184
|
+
if old and new and old != new:
|
|
185
|
+
updated = updated.replace(old, new)
|
|
186
|
+
return updated
|
|
187
|
+
|
|
188
|
+
|
|
149
189
|
def _target_path(client: str, *, user_home: Path | None = None) -> Path:
|
|
150
190
|
spec = BOOTSTRAP_SPECS[client]
|
|
151
191
|
home = user_home or _user_home()
|
|
@@ -243,12 +283,23 @@ def sync_client_bootstrap(
|
|
|
243
283
|
|
|
244
284
|
existing = target_path.read_text()
|
|
245
285
|
if CORE_START in existing and CORE_END in existing and USER_START in existing and USER_END in existing:
|
|
246
|
-
|
|
286
|
+
user_payload = _extract_user_payload(existing) or _extract_user_payload(rendered)
|
|
287
|
+
user_payload = _migrate_legacy_nexo_home_references(
|
|
288
|
+
user_payload,
|
|
289
|
+
nexo_home=nexo_home_path,
|
|
290
|
+
user_home=home_path,
|
|
291
|
+
)
|
|
292
|
+
user_block = _build_user_block(user_payload, rendered)
|
|
247
293
|
updated = _replace_block(existing, CORE_START, CORE_END, rendered_core)
|
|
248
294
|
updated = _replace_block(updated, USER_START, USER_END, user_block)
|
|
249
295
|
action = "updated" if updated != existing else "unchanged"
|
|
250
296
|
else:
|
|
251
297
|
legacy_user = _legacy_user_payload(existing)
|
|
298
|
+
legacy_user = _migrate_legacy_nexo_home_references(
|
|
299
|
+
legacy_user,
|
|
300
|
+
nexo_home=nexo_home_path,
|
|
301
|
+
user_home=home_path,
|
|
302
|
+
)
|
|
252
303
|
updated = _replace_block(rendered, USER_START, USER_END, _build_user_block(legacy_user, rendered))
|
|
253
304
|
action = "migrated"
|
|
254
305
|
|
package/src/cli.py
CHANGED
|
@@ -40,7 +40,9 @@ import subprocess
|
|
|
40
40
|
import sys
|
|
41
41
|
from pathlib import Path
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
from runtime_home import export_resolved_nexo_home
|
|
44
|
+
|
|
45
|
+
NEXO_HOME = export_resolved_nexo_home()
|
|
44
46
|
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
|
|
45
47
|
TERMINAL_CLIENT_LABELS = {
|
|
46
48
|
"claude_code": "Claude Code",
|
package/src/client_sync.py
CHANGED
|
@@ -18,6 +18,7 @@ except ModuleNotFoundError: # Python < 3.11
|
|
|
18
18
|
import tomli as tomllib
|
|
19
19
|
|
|
20
20
|
from bootstrap_docs import sync_client_bootstrap
|
|
21
|
+
from runtime_home import resolve_nexo_home
|
|
21
22
|
|
|
22
23
|
try:
|
|
23
24
|
from client_preferences import (
|
|
@@ -73,7 +74,7 @@ def _user_home() -> Path:
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
def _default_nexo_home() -> Path:
|
|
76
|
-
return
|
|
77
|
+
return resolve_nexo_home(os.environ.get("NEXO_HOME", str(_user_home() / ".nexo")))
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
|
|
@@ -12,15 +12,30 @@ import os
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
14
|
from db._core import get_db
|
|
15
|
+
from runtime_home import resolve_nexo_home
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
NEXO_HOME =
|
|
18
|
+
NEXO_HOME = resolve_nexo_home()
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _now_text() -> str:
|
|
21
22
|
return datetime.datetime.now().isoformat(timespec="seconds")
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
def _canonical_scripts_dir() -> Path:
|
|
26
|
+
home = resolve_nexo_home(os.environ.get("NEXO_HOME", str(NEXO_HOME)))
|
|
27
|
+
return home / "scripts"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _normalize_script_path(path: str | Path) -> str:
|
|
31
|
+
candidate = Path(path).expanduser()
|
|
32
|
+
try:
|
|
33
|
+
relative = candidate.resolve(strict=False).relative_to(_canonical_scripts_dir().resolve(strict=False))
|
|
34
|
+
except Exception:
|
|
35
|
+
return str(candidate)
|
|
36
|
+
return str(_canonical_scripts_dir() / relative)
|
|
37
|
+
|
|
38
|
+
|
|
24
39
|
def _row_to_dict(row) -> dict:
|
|
25
40
|
return dict(row) if row is not None else {}
|
|
26
41
|
|
|
@@ -61,6 +76,7 @@ def _safe_slug(value: str) -> str:
|
|
|
61
76
|
|
|
62
77
|
|
|
63
78
|
def _ensure_script_id(conn, name: str, path: str) -> str:
|
|
79
|
+
path = _normalize_script_path(path)
|
|
64
80
|
existing = conn.execute(
|
|
65
81
|
"SELECT id FROM personal_scripts WHERE path = ? LIMIT 1",
|
|
66
82
|
(path,),
|
|
@@ -90,6 +106,7 @@ def upsert_personal_script(
|
|
|
90
106
|
has_inline_metadata: bool = False,
|
|
91
107
|
) -> dict:
|
|
92
108
|
conn = get_db()
|
|
109
|
+
path = _normalize_script_path(path)
|
|
93
110
|
script_id = _ensure_script_id(conn, name, path)
|
|
94
111
|
now = _now_text()
|
|
95
112
|
conn.execute(
|
|
@@ -132,11 +149,12 @@ def upsert_personal_script(
|
|
|
132
149
|
|
|
133
150
|
def delete_missing_personal_scripts(active_paths: list[str]) -> int:
|
|
134
151
|
conn = get_db()
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
normalized_paths = [_normalize_script_path(path) for path in active_paths]
|
|
153
|
+
if normalized_paths:
|
|
154
|
+
placeholders = ",".join("?" for _ in normalized_paths)
|
|
137
155
|
rows = conn.execute(
|
|
138
156
|
f"SELECT id FROM personal_scripts WHERE path NOT IN ({placeholders})",
|
|
139
|
-
tuple(
|
|
157
|
+
tuple(normalized_paths),
|
|
140
158
|
).fetchall()
|
|
141
159
|
else:
|
|
142
160
|
rows = conn.execute("SELECT id FROM personal_scripts").fetchall()
|
|
@@ -160,6 +178,7 @@ def register_personal_script_schedule(
|
|
|
160
178
|
enabled: bool = True,
|
|
161
179
|
) -> dict | None:
|
|
162
180
|
conn = get_db()
|
|
181
|
+
script_path = _normalize_script_path(script_path)
|
|
163
182
|
script = conn.execute(
|
|
164
183
|
"SELECT id FROM personal_scripts WHERE path = ?",
|
|
165
184
|
(script_path,),
|
|
@@ -342,6 +361,7 @@ def list_personal_scripts(include_disabled: bool = True) -> list[dict]:
|
|
|
342
361
|
|
|
343
362
|
def get_personal_script(name_or_path: str) -> dict | None:
|
|
344
363
|
conn = get_db()
|
|
364
|
+
normalized_path = _normalize_script_path(name_or_path)
|
|
345
365
|
row = conn.execute(
|
|
346
366
|
"""
|
|
347
367
|
SELECT * FROM personal_scripts
|
|
@@ -349,7 +369,7 @@ def get_personal_script(name_or_path: str) -> dict | None:
|
|
|
349
369
|
ORDER BY path = ? DESC
|
|
350
370
|
LIMIT 1
|
|
351
371
|
""",
|
|
352
|
-
(
|
|
372
|
+
(normalized_path, name_or_path, normalized_path),
|
|
353
373
|
).fetchone()
|
|
354
374
|
if not row:
|
|
355
375
|
return None
|
|
@@ -361,9 +381,10 @@ def get_personal_script(name_or_path: str) -> dict | None:
|
|
|
361
381
|
|
|
362
382
|
def delete_personal_script(name_or_path: str) -> int:
|
|
363
383
|
conn = get_db()
|
|
384
|
+
normalized_path = _normalize_script_path(name_or_path)
|
|
364
385
|
result = conn.execute(
|
|
365
386
|
"DELETE FROM personal_scripts WHERE path = ? OR name = ? OR id = ?",
|
|
366
|
-
(
|
|
387
|
+
(normalized_path, name_or_path, name_or_path),
|
|
367
388
|
)
|
|
368
389
|
return int(result.rowcount or 0)
|
|
369
390
|
|
|
@@ -371,13 +392,14 @@ def delete_personal_script(name_or_path: str) -> int:
|
|
|
371
392
|
def record_personal_script_run(name_or_path: str, exit_code: int, run_at: str | None = None) -> None:
|
|
372
393
|
conn = get_db()
|
|
373
394
|
run_at = run_at or _now_text()
|
|
395
|
+
normalized_path = _normalize_script_path(name_or_path)
|
|
374
396
|
conn.execute(
|
|
375
397
|
"""
|
|
376
398
|
UPDATE personal_scripts
|
|
377
399
|
SET last_run_at = ?, last_exit_code = ?, updated_at = ?
|
|
378
400
|
WHERE path = ? OR name = ?
|
|
379
401
|
""",
|
|
380
|
-
(run_at, exit_code, _now_text(),
|
|
402
|
+
(run_at, exit_code, _now_text(), normalized_path, name_or_path),
|
|
381
403
|
)
|
|
382
404
|
|
|
383
405
|
|
|
@@ -393,7 +415,7 @@ def sync_personal_scripts_registry(
|
|
|
393
415
|
scheduled = 0
|
|
394
416
|
|
|
395
417
|
for record in script_records:
|
|
396
|
-
path =
|
|
418
|
+
path = _normalize_script_path(record["path"])
|
|
397
419
|
active_paths.append(path)
|
|
398
420
|
upsert_personal_script(
|
|
399
421
|
name=record.get("name") or Path(path).stem,
|
|
@@ -84,6 +84,28 @@ def _release_root() -> Path:
|
|
|
84
84
|
return Path(NEXO_CODE)
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def _has_release_publish_context(release_root: Path) -> bool:
|
|
88
|
+
git_dir = release_root / ".git"
|
|
89
|
+
if git_dir.exists() or git_dir.is_file():
|
|
90
|
+
return True
|
|
91
|
+
if _recorded_source_root() is not None:
|
|
92
|
+
return True
|
|
93
|
+
try:
|
|
94
|
+
release_root_resolved = release_root.resolve()
|
|
95
|
+
nexo_home_resolved = NEXO_HOME.resolve()
|
|
96
|
+
except Exception:
|
|
97
|
+
release_root_resolved = release_root
|
|
98
|
+
nexo_home_resolved = NEXO_HOME
|
|
99
|
+
if release_root_resolved == nexo_home_resolved:
|
|
100
|
+
return False
|
|
101
|
+
has_release_files = (
|
|
102
|
+
(release_root / "package.json").is_file()
|
|
103
|
+
and (release_root / "CHANGELOG.md").is_file()
|
|
104
|
+
and (release_root / "scripts" / "sync_release_artifacts.py").is_file()
|
|
105
|
+
)
|
|
106
|
+
return has_release_files
|
|
107
|
+
|
|
108
|
+
|
|
87
109
|
def _package_json_path() -> Path:
|
|
88
110
|
if PACKAGE_JSON.is_file():
|
|
89
111
|
return PACKAGE_JSON
|
|
@@ -2660,13 +2682,27 @@ def check_release_artifact_sync() -> DoctorCheck:
|
|
|
2660
2682
|
if changelog_version:
|
|
2661
2683
|
evidence.append(f"top changelog version: {changelog_version}")
|
|
2662
2684
|
|
|
2685
|
+
release_root = _release_root()
|
|
2686
|
+
if not _has_release_publish_context(release_root):
|
|
2687
|
+
if version:
|
|
2688
|
+
evidence.append(f"package version: {version}")
|
|
2689
|
+
evidence.append(f"release root: {release_root}")
|
|
2690
|
+
evidence.append("packaged runtime without source repo; release artifact audit skipped")
|
|
2691
|
+
return DoctorCheck(
|
|
2692
|
+
id="runtime.release_artifacts",
|
|
2693
|
+
tier="runtime",
|
|
2694
|
+
status="healthy",
|
|
2695
|
+
severity="info",
|
|
2696
|
+
summary="Release artifact audit skipped for packaged runtime",
|
|
2697
|
+
evidence=evidence,
|
|
2698
|
+
)
|
|
2699
|
+
|
|
2663
2700
|
if version and changelog_version and version != changelog_version:
|
|
2664
2701
|
status = "critical"
|
|
2665
2702
|
severity = "error"
|
|
2666
2703
|
evidence.append("package/changelog release version mismatch")
|
|
2667
2704
|
repair_plan.append("Bump or align CHANGELOG.md before publishing")
|
|
2668
2705
|
|
|
2669
|
-
release_root = _release_root()
|
|
2670
2706
|
sync_script = release_root / "scripts" / "sync_release_artifacts.py"
|
|
2671
2707
|
if not sync_script.is_file():
|
|
2672
2708
|
status = "critical"
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
# Caches output for 1 hour to avoid regenerating on rapid successive sessions.
|
|
5
5
|
set -uo pipefail
|
|
6
6
|
|
|
7
|
+
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
8
|
+
|
|
7
9
|
# Fase 3 item 7: hook lifecycle observability — record duration + exit code
|
|
8
10
|
# in hook_runs on EXIT. Best-effort: a failure here must not break the hook.
|
|
9
11
|
NEXO_HOOK_START_MS=$(python3 -c "import time; print(int(time.time()*1000))" 2>/dev/null || echo 0)
|
|
@@ -18,8 +20,12 @@ _nexo_record_hook_run() {
|
|
|
18
20
|
duration_ms=$((now_ms - NEXO_HOOK_START_MS))
|
|
19
21
|
fi
|
|
20
22
|
fi
|
|
21
|
-
local recorder
|
|
22
|
-
|
|
23
|
+
local recorder=""
|
|
24
|
+
if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/scripts/nexo-hook-record.py" ]; then
|
|
25
|
+
recorder="${NEXO_CODE%/}/scripts/nexo-hook-record.py"
|
|
26
|
+
elif [ -f "$NEXO_HOME/scripts/nexo-hook-record.py" ]; then
|
|
27
|
+
recorder="$NEXO_HOME/scripts/nexo-hook-record.py"
|
|
28
|
+
fi
|
|
23
29
|
if [ -f "$recorder" ]; then
|
|
24
30
|
python3 "$recorder" record \
|
|
25
31
|
--hook "$NEXO_HOOK_NAME" \
|
|
@@ -30,8 +36,6 @@ _nexo_record_hook_run() {
|
|
|
30
36
|
fi
|
|
31
37
|
}
|
|
32
38
|
trap _nexo_record_hook_run EXIT
|
|
33
|
-
|
|
34
|
-
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
35
39
|
BRIEFING_FILE="$NEXO_HOME/coordination/session-briefing.txt"
|
|
36
40
|
MAX_AGE_SECONDS=3600 # 1 hour cache
|
|
37
41
|
|
package/src/plugins/update.py
CHANGED
|
@@ -9,6 +9,8 @@ import sys
|
|
|
9
9
|
import time
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
+
from runtime_home import export_resolved_nexo_home
|
|
13
|
+
|
|
12
14
|
# Code root is the parent of plugins/:
|
|
13
15
|
# - source checkout: <repo>/src
|
|
14
16
|
# - packaged runtime: <NEXO_HOME>
|
|
@@ -16,7 +18,7 @@ _THIS_DIR = Path(__file__).resolve().parent
|
|
|
16
18
|
CODE_ROOT = _THIS_DIR.parent
|
|
17
19
|
_REPO_CANDIDATE = CODE_ROOT.parent
|
|
18
20
|
|
|
19
|
-
NEXO_HOME =
|
|
21
|
+
NEXO_HOME = export_resolved_nexo_home()
|
|
20
22
|
DATA_DIR = NEXO_HOME / "data"
|
|
21
23
|
BACKUP_BASE = NEXO_HOME / "backups"
|
|
22
24
|
|
|
@@ -330,8 +332,23 @@ def _backup_code_tree() -> tuple[str | None, str | None]:
|
|
|
330
332
|
timestamp = time.strftime("%Y-%m-%d-%H%M%S")
|
|
331
333
|
backup_dir = BACKUP_BASE / f"code-tree-{timestamp}"
|
|
332
334
|
# Directories and flat files that postinstall copies into NEXO_HOME
|
|
333
|
-
code_dirs = [
|
|
334
|
-
|
|
335
|
+
code_dirs = [
|
|
336
|
+
"bin",
|
|
337
|
+
"hooks",
|
|
338
|
+
"plugins",
|
|
339
|
+
"db",
|
|
340
|
+
"cognitive",
|
|
341
|
+
"dashboard",
|
|
342
|
+
"rules",
|
|
343
|
+
"crons",
|
|
344
|
+
"scripts",
|
|
345
|
+
"doctor",
|
|
346
|
+
"skills",
|
|
347
|
+
"skills-core",
|
|
348
|
+
"skills-runtime",
|
|
349
|
+
"templates",
|
|
350
|
+
]
|
|
351
|
+
code_files_glob = ["*.py", "requirements.txt", "package.json"]
|
|
335
352
|
try:
|
|
336
353
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
337
354
|
# Backup directories
|
|
@@ -372,6 +389,54 @@ def _restore_code_tree(backup_dir: str) -> str | None:
|
|
|
372
389
|
return None
|
|
373
390
|
|
|
374
391
|
|
|
392
|
+
def _normalize_preferences_for_client_sync() -> dict:
|
|
393
|
+
from client_preferences import normalize_client_preferences
|
|
394
|
+
|
|
395
|
+
schedule_path = NEXO_HOME / "config" / "schedule.json"
|
|
396
|
+
schedule_payload = json.loads(schedule_path.read_text()) if schedule_path.exists() else {}
|
|
397
|
+
normalized_preferences = normalize_client_preferences(schedule_payload)
|
|
398
|
+
if normalized_preferences != {
|
|
399
|
+
key: schedule_payload.get(key)
|
|
400
|
+
for key in normalized_preferences
|
|
401
|
+
}:
|
|
402
|
+
merged_schedule = dict(schedule_payload)
|
|
403
|
+
merged_schedule.update(normalized_preferences)
|
|
404
|
+
schedule_path.parent.mkdir(parents=True, exist_ok=True)
|
|
405
|
+
schedule_path.write_text(json.dumps(merged_schedule, indent=2, ensure_ascii=False) + "\n")
|
|
406
|
+
return normalized_preferences
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _sync_packaged_clients() -> tuple[bool, str | None]:
|
|
410
|
+
try:
|
|
411
|
+
from client_sync import sync_all_clients
|
|
412
|
+
except Exception as e:
|
|
413
|
+
return False, f"client sync import failed: {e}"
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
preferences = _normalize_preferences_for_client_sync()
|
|
417
|
+
result = sync_all_clients(
|
|
418
|
+
nexo_home=NEXO_HOME,
|
|
419
|
+
runtime_root=NEXO_HOME,
|
|
420
|
+
operator_name=os.environ.get("NEXO_NAME", ""),
|
|
421
|
+
preferences=preferences,
|
|
422
|
+
)
|
|
423
|
+
except Exception as e:
|
|
424
|
+
return False, f"client sync failed: {e}"
|
|
425
|
+
|
|
426
|
+
if result.get("ok"):
|
|
427
|
+
return True, None
|
|
428
|
+
|
|
429
|
+
clients = result.get("clients", {})
|
|
430
|
+
failures = []
|
|
431
|
+
for key, payload in clients.items():
|
|
432
|
+
if payload.get("ok") or payload.get("skipped"):
|
|
433
|
+
continue
|
|
434
|
+
failures.append(f"{key}: {payload.get('error', 'unknown error')}")
|
|
435
|
+
if not failures:
|
|
436
|
+
failures.append("unknown client sync failure")
|
|
437
|
+
return False, "; ".join(failures)
|
|
438
|
+
|
|
439
|
+
|
|
375
440
|
def _rollback_npm_package(target_version: str) -> str | None:
|
|
376
441
|
"""Rollback nexo-brain npm package to a specific version.
|
|
377
442
|
|
|
@@ -480,6 +545,20 @@ def _handle_packaged_update(progress_fn=None) -> str:
|
|
|
480
545
|
if verify_err:
|
|
481
546
|
errors.append(f"verification: {verify_err}")
|
|
482
547
|
|
|
548
|
+
hook_sync_warning = None
|
|
549
|
+
try:
|
|
550
|
+
_emit_progress(progress_fn, "Refreshing installed hooks and manifests...")
|
|
551
|
+
_refresh_installed_manifest()
|
|
552
|
+
_sync_hooks_to_home()
|
|
553
|
+
except Exception as e:
|
|
554
|
+
hook_sync_warning = f"{e}"
|
|
555
|
+
|
|
556
|
+
client_sync_warning = None
|
|
557
|
+
_emit_progress(progress_fn, "Refreshing shared client configs...")
|
|
558
|
+
clients_ok, client_sync_error = _sync_packaged_clients()
|
|
559
|
+
if not clients_ok:
|
|
560
|
+
client_sync_warning = client_sync_error or "unknown client sync error"
|
|
561
|
+
|
|
483
562
|
if errors:
|
|
484
563
|
# 5. Full rollback: restore code tree + DBs + pip deps + rollback npm package
|
|
485
564
|
if code_backup_dir:
|
|
@@ -516,6 +595,14 @@ def _handle_packaged_update(progress_fn=None) -> str:
|
|
|
516
595
|
lines = ["UPDATE SUCCESSFUL (packaged install)"]
|
|
517
596
|
lines.append(f" Version: {old_version} -> {new_version}")
|
|
518
597
|
lines.append(f" Backup: {backup_dir}")
|
|
598
|
+
if not hook_sync_warning:
|
|
599
|
+
lines.append(" Hooks: synced to NEXO_HOME")
|
|
600
|
+
else:
|
|
601
|
+
lines.append(f" WARNING: hook sync: {hook_sync_warning}")
|
|
602
|
+
if not client_sync_warning:
|
|
603
|
+
lines.append(" Clients: configured client targets synced")
|
|
604
|
+
else:
|
|
605
|
+
lines.append(f" WARNING: client sync: {client_sync_warning}")
|
|
519
606
|
lines.append("")
|
|
520
607
|
lines.append("MCP server restart needed to load new code.")
|
|
521
608
|
return "\n".join(lines)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Shared helpers to resolve the managed NEXO home path."""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def user_home() -> Path:
|
|
10
|
+
return Path(os.environ.get("HOME", str(Path.home()))).expanduser()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def managed_nexo_home(*, home: Path | None = None) -> Path:
|
|
14
|
+
return (home or user_home()) / ".nexo"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def legacy_nexo_home(*, home: Path | None = None) -> Path:
|
|
18
|
+
return (home or user_home()) / "claude"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_nexo_home(value: str | os.PathLike[str] | None = None) -> Path:
|
|
22
|
+
home = user_home()
|
|
23
|
+
managed = managed_nexo_home(home=home)
|
|
24
|
+
candidate = Path(value).expanduser() if value else Path(
|
|
25
|
+
os.environ.get("NEXO_HOME", str(managed))
|
|
26
|
+
).expanduser()
|
|
27
|
+
legacy = legacy_nexo_home(home=home)
|
|
28
|
+
|
|
29
|
+
if candidate == managed:
|
|
30
|
+
return managed
|
|
31
|
+
if candidate == legacy:
|
|
32
|
+
return managed if managed.exists() or legacy.is_symlink() else candidate
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
if managed.exists() and candidate.resolve(strict=False) == managed.resolve(strict=False):
|
|
36
|
+
return managed
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
return candidate
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def export_resolved_nexo_home(value: str | os.PathLike[str] | None = None) -> Path:
|
|
44
|
+
resolved = resolve_nexo_home(value)
|
|
45
|
+
os.environ["NEXO_HOME"] = str(resolved)
|
|
46
|
+
return resolved
|
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
|
|
@@ -73,6 +75,7 @@ METADATA_KEYS = {
|
|
|
73
75
|
"run_on_wake",
|
|
74
76
|
"idempotent",
|
|
75
77
|
"max_catchup_age",
|
|
78
|
+
"doctor_allow_db",
|
|
76
79
|
}
|
|
77
80
|
SUPPORTED_RUNTIMES = {"python", "shell", "node", "php", "unknown"}
|
|
78
81
|
PERSONAL_SCHEDULE_MANAGED_ENV = "NEXO_MANAGED_PERSONAL_CRON"
|
|
@@ -213,7 +216,11 @@ def _is_ignored(path: Path) -> bool:
|
|
|
213
216
|
return True
|
|
214
217
|
if path.name.startswith("."):
|
|
215
218
|
return True
|
|
216
|
-
|
|
219
|
+
try:
|
|
220
|
+
relative_path = path.resolve().relative_to(get_scripts_dir().resolve())
|
|
221
|
+
except Exception:
|
|
222
|
+
return False
|
|
223
|
+
for parent in relative_path.parents:
|
|
217
224
|
if parent.name in _IGNORED_DIRS:
|
|
218
225
|
return True
|
|
219
226
|
return False
|
|
@@ -1425,14 +1432,19 @@ def doctor_script(path_or_name: str) -> dict:
|
|
|
1425
1432
|
elif cmd:
|
|
1426
1433
|
items.append({"level": "pass", "msg": f"Required command found: {cmd}"})
|
|
1427
1434
|
|
|
1435
|
+
allow_db_access = str(meta.get("doctor_allow_db", "")).strip().lower() in {"1", "true", "yes", "on"}
|
|
1436
|
+
if allow_db_access:
|
|
1437
|
+
items.append({"level": "pass", "msg": "Doctor DB access explicitly allowed"})
|
|
1438
|
+
|
|
1428
1439
|
# Forbidden patterns (only for personal scripts)
|
|
1429
1440
|
if not is_core:
|
|
1430
1441
|
try:
|
|
1431
1442
|
content = p.read_text(errors="ignore")
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1443
|
+
if not allow_db_access:
|
|
1444
|
+
for pat in _FORBIDDEN_PATTERNS:
|
|
1445
|
+
match = pat.search(content)
|
|
1446
|
+
if match:
|
|
1447
|
+
items.append({"level": "fail", "msg": f"Forbidden DB pattern found: {match.group()}"})
|
|
1436
1448
|
except Exception:
|
|
1437
1449
|
pass
|
|
1438
1450
|
|
|
@@ -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.
|
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()
|