nexo-brain 5.3.10 → 5.3.12
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 +5 -7
- package/package.json +1 -1
- package/src/agent_runner.py +2 -0
- package/src/auto_update.py +116 -4
- package/src/cli.py +8 -2
- package/src/client_preferences.py +26 -7
- package/src/client_sync.py +3 -2
- package/src/db/__init__.py +4 -1
- package/src/db/_protocol.py +28 -3
- package/src/doctor/orchestrator.py +10 -1
- package/src/doctor/planes.py +87 -0
- package/src/hook_guardrails.py +147 -11
- package/src/plugins/cortex.py +37 -3
- package/src/plugins/doctor.py +3 -2
- package/src/plugins/protocol.py +41 -3
- package/src/plugins/schedule.py +119 -1
- package/src/runtime_power.py +3 -2
- package/src/scripts/nexo-cortex-cycle.py +48 -21
- package/src/scripts/nexo-daily-self-audit.py +49 -22
- package/src/scripts/rehydrate_learnings_from_archive.py +245 -0
- package/src/server.py +2 -0
- package/src/skills/run-nexo-core-fix-cycle/guide.md +17 -0
- package/src/skills/run-nexo-core-fix-cycle/script.py +276 -0
- package/src/skills/run-nexo-core-fix-cycle/skill.json +58 -0
- package/src/skills/run-release-final-audit/guide.md +5 -3
- package/src/skills/run-release-final-audit/script.py +17 -8
- package/src/skills/run-release-final-audit/skill.json +15 -2
- package/src/skills/run-runtime-doctor/script.py +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.12",
|
|
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
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `5.3.
|
|
21
|
+
Version `5.3.11` is the current packaged-runtime line: protocol and Cortex now reject malformed `outcome`, `task_type`, and `impact_level` values explicitly instead of silently coercing them into other valid states, so task history, debt, hot context, and decision telemetry stay trustworthy even when a caller passes a bad contract payload.
|
|
22
22
|
|
|
23
23
|
Start here:
|
|
24
24
|
- [5-minute quickstart](docs/quickstart-5-minutes.md)
|
|
@@ -89,7 +89,7 @@ Versions `3.1.7` through `3.2.0` close the recent-memory gap:
|
|
|
89
89
|
- when even that misses, NEXO now exposes raw transcript fallback tools for Claude Code and Codex session stores
|
|
90
90
|
- NEXO can now inspect itself through a live system catalog derived from canonical sources instead of relying only on stale docs or operator memory
|
|
91
91
|
|
|
92
|
-
Version `5.3.10`
|
|
92
|
+
Version `5.3.11` hardens protocol and Cortex contracts: malformed `outcome`, `task_type`, and `impact_level` values now fail explicitly instead of being coerced into other valid states, so persisted task history, debt, hot context, and decision telemetry stay faithful to what the caller actually asked for. Version `5.3.10` tightened the packaged-runtime truth layer again: installs and updates now keep `~/.nexo/package.json` aligned with the published npm package so runtime metadata and doctor evidence no longer drift to an old version, `nexo doctor --tier deep` treats a missing `self-audit-summary.json` as a pending bootstrap artifact when the runtime was just installed or updated instead of reporting a false degradation, weekly Evolution now asks for explicit `dimension_scores` / `score_evidence` so telemetry can persist instead of staying blank, and daily synthesis only ingests `update-last-summary.json` when it carries actionable runtime signals. Version `5.3.9` is the packaged core-artifact manifest heal for `5.3.8`: packaged updates now rebuild `runtime-core-artifacts.json` from the canonical npm package `src/` tree instead of scanning the live `~/.nexo/scripts` directory, script classification prefers that canonical packaged source when available, and runtime doctor syncs personal scripts before LaunchAgent inventory so personal automations recover cleanly instead of being mistaken for unknown core drift. Version `5.3.8` was the immediate packaged-migration hotfix for `5.3.7`: the installer/runtime migrator now discovers all top-level runtime Python modules from `src/` dynamically instead of relying on a manual allowlist, so new product surfaces like `nexo export` / `nexo import` actually arrive in `~/.nexo` after update instead of being present only in the published npm tarball. Version `5.3.7` closed the remaining packaged-runtime happy-path gap and finally exposed portable user-data migration commands: packaged `nexo update` now self-heals cron definitions and LaunchAgents after a successful npm bump, new `nexo export` / `nexo import` commands move operator data as a safe bundle instead of leaving that flow implicit, and runtime doctor now distinguishes tracked historical Codex drift from an actually broken runtime so cleaned installs stop staying red for stale transcript debt alone. Version `5.3.6` hardened the Claude Code bootstrap path and related runtime hygiene: managed client sync now writes the NEXO MCP server where current Claude Code actually reads it (`~/.claude.json`), script classification is stricter about core-vs-personal runtime artifacts, schedule status distinguishes genuinely running jobs from broken ones, and retroactive learnings stop opening keyword-only false positives outside their declared `applies_to` scope. Version `5.3.5` already keeps CLI version visibility honest right after `nexo update`: if the cached npm version lags behind the runtime you just installed, `nexo` / `nexo chat` now clamp `Latest` to the installed version and refresh the cache instead of showing a stale older release. Version `5.3.4` already cleaned up legacy core alias leakage and added the version-status banner. Version `5.3.3` closed the remaining packaged-runtime doctor mismatch: the built-in hourly backup helper is now inventoried as a core LaunchAgent, so clean installs no longer get a false unknown-LaunchAgent warning. Version `5.3.2` already hardened the runtime boundary by persisting which runtime scripts/hooks are core product artifacts, keeping `nexo scripts` from mixing those into the personal bucket, and migrating the legacy Claude Code heartbeat wrappers into managed core hooks.
|
|
93
93
|
|
|
94
94
|
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.
|
|
95
95
|
|
|
@@ -821,9 +821,7 @@ If you want the shell or Python wrappers instead of raw MCP tools:
|
|
|
821
821
|
- [docs/reference-verticals.md](docs/reference-verticals.md)
|
|
822
822
|
- [compare/README.md](compare/README.md)
|
|
823
823
|
|
|
824
|
-
|
|
825
|
-
- Claude Code: `Opus 4.6 with 1M context`
|
|
826
|
-
- Codex: `gpt-5.4` with `xhigh` reasoning
|
|
824
|
+
The model you pick during install is used everywhere — interactive sessions, automation scripts, and all task profiles. Change it once in your preferences and every part of the system follows. Default: `Opus 4.6 with 1M context`.
|
|
827
825
|
|
|
828
826
|
Or use the shell alias created during install (e.g. `atlas`), which now runs `nexo chat .` so it opens the terminal client you pick for that session, with the last-used option shown first.
|
|
829
827
|
|
|
@@ -904,7 +902,7 @@ The Doctor system reads existing health artifacts (immune, watchdog, self-audit)
|
|
|
904
902
|
- **macOS or Linux** (Windows via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install))
|
|
905
903
|
- **Node.js 18+** (for the installer)
|
|
906
904
|
- **Claude Code is the primary recommended client.** It remains the most mature NEXO path: native hooks, the most battle-tested automation contract, and the clearest parity with historical production behavior.
|
|
907
|
-
- **
|
|
905
|
+
- **Model:** You pick your model during install and every component uses it. Default is `Opus 4.6 with 1M context`. Scripts and automation profiles read from a single preference — no hardcoded model strings.
|
|
908
906
|
- Python 3, Homebrew, and the selected required client/backend can be installed automatically when NEXO has a supported installer path for that dependency.
|
|
909
907
|
|
|
910
908
|
## Architecture
|
|
@@ -1021,7 +1019,7 @@ When Claude Desktop is installed, `nexo-brain`, `nexo update`, and `nexo clients
|
|
|
1021
1019
|
|
|
1022
1020
|
### Codex
|
|
1023
1021
|
|
|
1024
|
-
When Codex CLI is available, `nexo-brain`, `nexo update`, and `nexo clients sync` register the same `nexo` MCP server via `codex mcp add`, so Codex uses the same local memory store as Claude Code and Claude Desktop. If selected during install, `nexo chat` can open Codex directly and background automation can also run through Codex. Interactive `nexo chat` launches use Codex's aggressive no-confirmation mode so the session does not stall on repetitive approval prompts.
|
|
1022
|
+
When Codex CLI is available, `nexo-brain`, `nexo update`, and `nexo clients sync` register the same `nexo` MCP server via `codex mcp add`, so Codex uses the same local memory store as Claude Code and Claude Desktop. If selected during install, `nexo chat` can open Codex directly and background automation can also run through Codex. Interactive `nexo chat` launches use Codex's aggressive no-confirmation mode so the session does not stall on repetitive approval prompts. Codex uses the same model you configured during install — no separate model override is needed. Runtime Doctor also audits recent Codex sessions for NEXO startup markers and conditioned-file protocol discipline so parity drift does not hide behind the lack of native Claude-style hooks.
|
|
1025
1023
|
|
|
1026
1024
|
### Cursor
|
|
1027
1025
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.12",
|
|
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",
|
package/src/agent_runner.py
CHANGED
|
@@ -509,6 +509,8 @@ def _build_codex_prompt(
|
|
|
509
509
|
"If that tool is unavailable, call `nexo_guard_check(...)` and `nexo_cortex_check(...)` first.\n"
|
|
510
510
|
"- For long multi-step or cross-session work, call `nexo_workflow_open(...)` and keep it updated with "
|
|
511
511
|
"`nexo_workflow_update(...)` so resume/replay use durable state instead of guesswork.\n"
|
|
512
|
+
"- Before diagnosing NEXO, explicitly fix the plane first: `product_public`, `runtime_personal`, `installation_live`, `database_real`, or `cooperator`. "
|
|
513
|
+
"Do not mix planes inside the same diagnosis.\n"
|
|
512
514
|
"- If a target file has conditioned learnings or blocking guard rules, review them before any read/edit/delete step, and acknowledge guard before any edit/delete step.\n"
|
|
513
515
|
"- Do not claim done without explicit verification evidence. Close with `nexo_task_close(...)`; if unavailable, capture the change log and state the evidence explicitly.\n"
|
|
514
516
|
"- When a correction changes the canonical rule, capture or supersede the learning instead of leaving contradictory active rules behind."
|
package/src/auto_update.py
CHANGED
|
@@ -36,6 +36,7 @@ TEMPLATE_FILE = REPO_DIR / "templates" / "CLAUDE.md.template"
|
|
|
36
36
|
|
|
37
37
|
CHECK_COOLDOWN_SECONDS = 3600 # 1 hour
|
|
38
38
|
GIT_TIMEOUT_SECONDS = 4 # stay well under the 5s total budget
|
|
39
|
+
CRITICAL_BACKUP_TABLES = ("learnings", "session_diary", "guard_checks", "protocol_debt")
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def _log(msg: str):
|
|
@@ -43,6 +44,108 @@ def _log(msg: str):
|
|
|
43
44
|
print(f"[NEXO auto-update] {msg}", file=sys.stderr)
|
|
44
45
|
|
|
45
46
|
|
|
47
|
+
def _critical_table_count(db_path: Path, table: str) -> int | None:
|
|
48
|
+
"""Return COUNT(*) for a critical table when it exists, otherwise None."""
|
|
49
|
+
import sqlite3
|
|
50
|
+
|
|
51
|
+
conn = None
|
|
52
|
+
try:
|
|
53
|
+
conn = sqlite3.connect(str(db_path))
|
|
54
|
+
exists = conn.execute(
|
|
55
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name = ?",
|
|
56
|
+
(table,),
|
|
57
|
+
).fetchone()
|
|
58
|
+
if not exists:
|
|
59
|
+
return None
|
|
60
|
+
row = conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()
|
|
61
|
+
return int(row[0]) if row else 0
|
|
62
|
+
except Exception:
|
|
63
|
+
return None
|
|
64
|
+
finally:
|
|
65
|
+
if conn is not None:
|
|
66
|
+
try:
|
|
67
|
+
conn.close()
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _find_primary_db_path() -> Path | None:
|
|
73
|
+
"""Return the main nexo.db path if present."""
|
|
74
|
+
for candidate in (DATA_DIR / "nexo.db", NEXO_HOME / "nexo.db", SRC_DIR / "nexo.db"):
|
|
75
|
+
if candidate.is_file():
|
|
76
|
+
return candidate
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _validate_db_backup(source_db: Path, backup_db: Path) -> dict:
|
|
81
|
+
"""Check that a backup preserves non-empty critical tables from the source DB."""
|
|
82
|
+
report = {
|
|
83
|
+
"ok": True,
|
|
84
|
+
"source_db": str(source_db),
|
|
85
|
+
"backup_db": str(backup_db),
|
|
86
|
+
"source_counts": {},
|
|
87
|
+
"backup_counts": {},
|
|
88
|
+
"regressions": [],
|
|
89
|
+
"errors": [],
|
|
90
|
+
}
|
|
91
|
+
if not source_db.is_file():
|
|
92
|
+
report["ok"] = False
|
|
93
|
+
report["errors"].append(f"source db missing: {source_db}")
|
|
94
|
+
return report
|
|
95
|
+
if not backup_db.is_file():
|
|
96
|
+
report["ok"] = False
|
|
97
|
+
report["errors"].append(f"backup db missing: {backup_db}")
|
|
98
|
+
return report
|
|
99
|
+
|
|
100
|
+
for table in CRITICAL_BACKUP_TABLES:
|
|
101
|
+
source_count = _critical_table_count(source_db, table)
|
|
102
|
+
backup_count = _critical_table_count(backup_db, table)
|
|
103
|
+
report["source_counts"][table] = source_count
|
|
104
|
+
report["backup_counts"][table] = backup_count
|
|
105
|
+
|
|
106
|
+
if source_count is None:
|
|
107
|
+
continue
|
|
108
|
+
if backup_count is None:
|
|
109
|
+
report["regressions"].append({
|
|
110
|
+
"table": table,
|
|
111
|
+
"source": source_count,
|
|
112
|
+
"backup": None,
|
|
113
|
+
"reason": "missing_in_backup",
|
|
114
|
+
})
|
|
115
|
+
continue
|
|
116
|
+
if source_count > 0 and backup_count == 0:
|
|
117
|
+
report["regressions"].append({
|
|
118
|
+
"table": table,
|
|
119
|
+
"source": source_count,
|
|
120
|
+
"backup": backup_count,
|
|
121
|
+
"reason": "critical_rows_lost",
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if report["regressions"] or report["errors"]:
|
|
125
|
+
report["ok"] = False
|
|
126
|
+
return report
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _create_validated_db_backup() -> tuple[str | None, dict | None]:
|
|
130
|
+
"""Create a DB backup and validate that critical tables still contain data."""
|
|
131
|
+
backup_dir = _backup_dbs()
|
|
132
|
+
if not backup_dir:
|
|
133
|
+
return None, None
|
|
134
|
+
|
|
135
|
+
source_db = _find_primary_db_path()
|
|
136
|
+
if source_db is None:
|
|
137
|
+
return backup_dir, None
|
|
138
|
+
|
|
139
|
+
report = _validate_db_backup(source_db, Path(backup_dir) / source_db.name)
|
|
140
|
+
if not report["ok"]:
|
|
141
|
+
details = ", ".join(
|
|
142
|
+
f"{item['table']} {item['source']}->{item['backup']}"
|
|
143
|
+
for item in report["regressions"]
|
|
144
|
+
) or "; ".join(report["errors"])
|
|
145
|
+
_log(f"DB backup validation failed: {details}")
|
|
146
|
+
return backup_dir, report
|
|
147
|
+
|
|
148
|
+
|
|
46
149
|
def _read_last_check() -> dict:
|
|
47
150
|
"""Read last check state from disk."""
|
|
48
151
|
try:
|
|
@@ -677,6 +780,11 @@ def _check_git_updates() -> str | None:
|
|
|
677
780
|
if rc != 0:
|
|
678
781
|
return None
|
|
679
782
|
|
|
783
|
+
db_backup_dir, backup_report = _create_validated_db_backup()
|
|
784
|
+
if backup_report is not None and not backup_report["ok"]:
|
|
785
|
+
_log("Skipping auto-update because the validated pre-update DB backup is not trustworthy.")
|
|
786
|
+
return None
|
|
787
|
+
|
|
680
788
|
rc, pull_out, pull_err = _git("pull", "--ff-only")
|
|
681
789
|
if rc != 0:
|
|
682
790
|
_log(f"git pull --ff-only failed: {pull_err}")
|
|
@@ -685,9 +793,6 @@ def _check_git_updates() -> str | None:
|
|
|
685
793
|
new_version = _read_package_version()
|
|
686
794
|
new_req_hash = _requirements_hash()
|
|
687
795
|
|
|
688
|
-
# Backup databases before any changes that might run migrations
|
|
689
|
-
db_backup_dir = _backup_dbs()
|
|
690
|
-
|
|
691
796
|
# Reinstall pip deps if requirements.txt content changed (not just version)
|
|
692
797
|
if old_req_hash != new_req_hash:
|
|
693
798
|
if not _reinstall_pip_deps():
|
|
@@ -2057,7 +2162,14 @@ def manual_sync_update(*, interactive: bool = False, allow_source_pull: bool = T
|
|
|
2057
2162
|
pulled = True
|
|
2058
2163
|
|
|
2059
2164
|
_emit_progress(progress_fn, "Creating runtime backups...")
|
|
2060
|
-
db_backup_dir =
|
|
2165
|
+
db_backup_dir, backup_report = _create_validated_db_backup()
|
|
2166
|
+
if backup_report is not None and not backup_report["ok"]:
|
|
2167
|
+
return {
|
|
2168
|
+
"ok": False,
|
|
2169
|
+
"mode": "sync",
|
|
2170
|
+
"error": "DB backup validation failed before runtime sync.",
|
|
2171
|
+
"backup_dir": db_backup_dir,
|
|
2172
|
+
}
|
|
2061
2173
|
tree_backup_dir = _backup_runtime_tree(NEXO_HOME)
|
|
2062
2174
|
sync_result = {"ok": False, "mode": "sync", "pulled_source": pulled, "backup_dir": db_backup_dir, "tree_backup": tree_backup_dir}
|
|
2063
2175
|
try:
|
package/src/cli.py
CHANGED
|
@@ -26,7 +26,7 @@ Entry points:
|
|
|
26
26
|
nexo skills evolution [--json]
|
|
27
27
|
nexo clients sync [--json]
|
|
28
28
|
nexo contributor status|on|off [--json]
|
|
29
|
-
nexo doctor [--tier boot|runtime|deep|all] [--json] [--fix]
|
|
29
|
+
nexo doctor [--tier boot|runtime|deep|all] [--plane runtime_personal|installation_live|database_real] [--json] [--fix]
|
|
30
30
|
nexo uninstall [--dry-run] [--delete-data] [--json]
|
|
31
31
|
"""
|
|
32
32
|
from __future__ import annotations
|
|
@@ -1210,7 +1210,7 @@ def _doctor(args):
|
|
|
1210
1210
|
init_db()
|
|
1211
1211
|
tier_label = getattr(args, "tier", "boot") or "boot"
|
|
1212
1212
|
print(f"[NEXO] Inspecting {tier_label} diagnostics... please wait.", file=sys.stderr, flush=True)
|
|
1213
|
-
report = run_doctor(tier=args.tier, fix=args.fix)
|
|
1213
|
+
report = run_doctor(tier=args.tier, fix=args.fix, plane=getattr(args, "plane", ""))
|
|
1214
1214
|
output = format_report(report, fmt="json" if args.json else "text")
|
|
1215
1215
|
print(output)
|
|
1216
1216
|
|
|
@@ -1749,6 +1749,12 @@ def main():
|
|
|
1749
1749
|
doctor_parser = sub.add_parser("doctor", help="Unified diagnostics")
|
|
1750
1750
|
doctor_parser.add_argument("--tier", default="boot", choices=["boot", "runtime", "deep", "all"],
|
|
1751
1751
|
help="Diagnostic tier (default: boot)")
|
|
1752
|
+
doctor_parser.add_argument(
|
|
1753
|
+
"--plane",
|
|
1754
|
+
default="",
|
|
1755
|
+
choices=["", "runtime_personal", "installation_live", "database_real", "product_public", "cooperator"],
|
|
1756
|
+
help="Diagnostic plane. Doctor only runs on runtime_personal, installation_live, or database_real.",
|
|
1757
|
+
)
|
|
1752
1758
|
doctor_parser.add_argument("--json", action="store_true", help="JSON output")
|
|
1753
1759
|
doctor_parser.add_argument("--fix", action="store_true", help="Apply deterministic fixes")
|
|
1754
1760
|
|
|
@@ -48,10 +48,13 @@ INSTALL_PREFERENCE_KEYS = {
|
|
|
48
48
|
}
|
|
49
49
|
DEFAULT_CLAUDE_CODE_MODEL = "claude-opus-4-6[1m]"
|
|
50
50
|
DEFAULT_CLAUDE_CODE_REASONING_EFFORT = ""
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
# Codex/fast defaults: fall back to the user's configured model, not a hardcoded
|
|
52
|
+
# third-party model. The user picks their model once at install time; every
|
|
53
|
+
# profile and backend should honour that choice.
|
|
54
|
+
DEFAULT_CODEX_MODEL = DEFAULT_CLAUDE_CODE_MODEL
|
|
55
|
+
DEFAULT_CODEX_REASONING_EFFORT = ""
|
|
56
|
+
DEFAULT_FAST_MODEL = DEFAULT_CLAUDE_CODE_MODEL
|
|
57
|
+
DEFAULT_FAST_REASONING_EFFORT = ""
|
|
55
58
|
|
|
56
59
|
|
|
57
60
|
def _user_home() -> Path:
|
|
@@ -260,9 +263,9 @@ def default_automation_task_profiles() -> dict[str, dict[str, str]]:
|
|
|
260
263
|
"reasoning_effort": "",
|
|
261
264
|
},
|
|
262
265
|
"fast": {
|
|
263
|
-
"backend":
|
|
264
|
-
"model":
|
|
265
|
-
"reasoning_effort":
|
|
266
|
+
"backend": "",
|
|
267
|
+
"model": "",
|
|
268
|
+
"reasoning_effort": "",
|
|
266
269
|
},
|
|
267
270
|
"balanced": {
|
|
268
271
|
"backend": "",
|
|
@@ -523,6 +526,22 @@ def resolve_terminal_client(requested: str | None = None, *, preferences: dict |
|
|
|
523
526
|
)
|
|
524
527
|
|
|
525
528
|
|
|
529
|
+
def resolve_user_model(preferences: dict | None = None) -> str:
|
|
530
|
+
"""Return the single model the user has configured.
|
|
531
|
+
|
|
532
|
+
Scripts and automation tasks should call this instead of hardcoding a model
|
|
533
|
+
string. The value comes from the user's ``client_runtime_profiles`` for
|
|
534
|
+
their ``default_terminal_client`` (usually ``claude_code``).
|
|
535
|
+
"""
|
|
536
|
+
normalized = preferences or load_client_preferences()
|
|
537
|
+
client = normalize_default_terminal_client(
|
|
538
|
+
normalized["default_terminal_client"],
|
|
539
|
+
interactive_clients=normalized["interactive_clients"],
|
|
540
|
+
)
|
|
541
|
+
profile = resolve_client_runtime_profile(client, preferences=normalized)
|
|
542
|
+
return profile["model"]
|
|
543
|
+
|
|
544
|
+
|
|
526
545
|
def resolve_automation_backend(preferences: dict | None = None) -> str:
|
|
527
546
|
normalized = preferences or load_client_preferences()
|
|
528
547
|
return normalize_automation_backend(
|
package/src/client_sync.py
CHANGED
|
@@ -61,9 +61,10 @@ except Exception:
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
def resolve_client_runtime_profile(client: str, preferences: dict | None = None) -> dict:
|
|
64
|
+
_default_model = "claude-opus-4-6[1m]"
|
|
64
65
|
defaults = {
|
|
65
|
-
"claude_code": {"model":
|
|
66
|
-
"codex": {"model":
|
|
66
|
+
"claude_code": {"model": _default_model, "reasoning_effort": ""},
|
|
67
|
+
"codex": {"model": _default_model, "reasoning_effort": ""},
|
|
67
68
|
}
|
|
68
69
|
return dict(defaults.get(client, {}))
|
|
69
70
|
|
package/src/db/__init__.py
CHANGED
|
@@ -147,13 +147,16 @@ from db._cron_runs import (
|
|
|
147
147
|
|
|
148
148
|
# Protocol discipline runtime
|
|
149
149
|
from db._protocol import (
|
|
150
|
+
VALID_IMPACT_LEVELS,
|
|
151
|
+
VALID_TASK_TYPES,
|
|
152
|
+
VALID_CLOSE_OUTCOMES,
|
|
150
153
|
create_protocol_task, get_protocol_task, close_protocol_task,
|
|
151
154
|
create_protocol_debt, resolve_protocol_debts, list_protocol_debts,
|
|
152
155
|
protocol_compliance_summary,
|
|
153
156
|
create_cortex_evaluation, get_cortex_evaluation, list_cortex_evaluations,
|
|
154
157
|
cortex_evaluation_summary,
|
|
155
158
|
latest_cortex_evaluation_for_task, task_has_cortex_evaluation,
|
|
156
|
-
override_cortex_evaluation,
|
|
159
|
+
override_cortex_evaluation, validate_close_outcome, validate_impact_level, validate_task_type,
|
|
157
160
|
)
|
|
158
161
|
|
|
159
162
|
# Durable workflow runtime
|
package/src/db/_protocol.py
CHANGED
|
@@ -9,6 +9,7 @@ from db._core import get_db
|
|
|
9
9
|
|
|
10
10
|
VALID_TASK_TYPES = {"answer", "analyze", "edit", "execute", "delegate"}
|
|
11
11
|
VALID_OUTCOMES = {"open", "done", "partial", "blocked", "failed", "cancelled"}
|
|
12
|
+
VALID_CLOSE_OUTCOMES = VALID_OUTCOMES - {"open"}
|
|
12
13
|
VALID_DEBT_STATUS = {"open", "forgiven", "resolved"}
|
|
13
14
|
VALID_IMPACT_LEVELS = {"medium", "high", "critical"}
|
|
14
15
|
|
|
@@ -33,6 +34,30 @@ def _row_to_dict(row):
|
|
|
33
34
|
return dict(row) if row else None
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
def validate_task_type(task_type: str) -> str:
|
|
38
|
+
clean_type = (task_type or "").strip()
|
|
39
|
+
if clean_type not in VALID_TASK_TYPES:
|
|
40
|
+
expected = ", ".join(sorted(VALID_TASK_TYPES))
|
|
41
|
+
raise ValueError(f"Invalid task_type '{clean_type or '<empty>'}'. Expected one of: {expected}.")
|
|
42
|
+
return clean_type
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_impact_level(impact_level: str) -> str:
|
|
46
|
+
clean_level = (impact_level or "").strip()
|
|
47
|
+
if clean_level not in VALID_IMPACT_LEVELS:
|
|
48
|
+
expected = ", ".join(sorted(VALID_IMPACT_LEVELS))
|
|
49
|
+
raise ValueError(f"Invalid impact_level '{clean_level or '<empty>'}'. Expected one of: {expected}.")
|
|
50
|
+
return clean_level
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_close_outcome(outcome: str) -> str:
|
|
54
|
+
clean_outcome = (outcome or "").strip()
|
|
55
|
+
if clean_outcome not in VALID_CLOSE_OUTCOMES:
|
|
56
|
+
expected = ", ".join(sorted(VALID_CLOSE_OUTCOMES))
|
|
57
|
+
raise ValueError(f"Invalid close outcome '{clean_outcome or '<empty>'}'. Expected one of: {expected}.")
|
|
58
|
+
return clean_outcome
|
|
59
|
+
|
|
60
|
+
|
|
36
61
|
def create_protocol_task(
|
|
37
62
|
session_id: str,
|
|
38
63
|
goal: str,
|
|
@@ -68,7 +93,7 @@ def create_protocol_task(
|
|
|
68
93
|
) -> dict:
|
|
69
94
|
conn = get_db()
|
|
70
95
|
task_id = _task_id()
|
|
71
|
-
clean_type = task_type
|
|
96
|
+
clean_type = validate_task_type(task_type)
|
|
72
97
|
conn.execute(
|
|
73
98
|
"""INSERT INTO protocol_tasks (
|
|
74
99
|
task_id, session_id, goal, task_type, area, project_hint, context_hint,
|
|
@@ -144,7 +169,7 @@ def create_cortex_evaluation(
|
|
|
144
169
|
selection_source: str = "recommended",
|
|
145
170
|
) -> dict:
|
|
146
171
|
conn = get_db()
|
|
147
|
-
clean_level = impact_level
|
|
172
|
+
clean_level = validate_impact_level(impact_level)
|
|
148
173
|
cursor = conn.execute(
|
|
149
174
|
"""INSERT INTO cortex_evaluations (
|
|
150
175
|
session_id, task_id, goal, task_type, area, impact_level, context_hint,
|
|
@@ -335,7 +360,7 @@ def close_protocol_task(
|
|
|
335
360
|
outcome_notes: str = "",
|
|
336
361
|
) -> dict:
|
|
337
362
|
conn = get_db()
|
|
338
|
-
clean_outcome = outcome
|
|
363
|
+
clean_outcome = validate_close_outcome(outcome)
|
|
339
364
|
conn.execute(
|
|
340
365
|
"""UPDATE protocol_tasks
|
|
341
366
|
SET status = ?,
|
|
@@ -6,6 +6,7 @@ import time
|
|
|
6
6
|
import traceback
|
|
7
7
|
|
|
8
8
|
from doctor.models import DoctorCheck, DoctorReport
|
|
9
|
+
from doctor.planes import diagnostic_plane_preflight
|
|
9
10
|
from doctor.providers.boot import run_boot_checks
|
|
10
11
|
from doctor.providers.runtime import run_runtime_checks
|
|
11
12
|
from doctor.providers.deep import run_deep_checks
|
|
@@ -22,12 +23,13 @@ _TIER_ORDER = ["boot", "runtime", "deep"]
|
|
|
22
23
|
VALID_TIERS = frozenset(_TIER_ORDER) | {"all"}
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def run_doctor(tier: str = "boot", fix: bool = False) -> DoctorReport:
|
|
26
|
+
def run_doctor(tier: str = "boot", fix: bool = False, plane: str = "") -> DoctorReport:
|
|
26
27
|
"""Run diagnostic checks for the specified tier(s).
|
|
27
28
|
|
|
28
29
|
Args:
|
|
29
30
|
tier: "boot", "runtime", "deep", or "all"
|
|
30
31
|
fix: If True, apply deterministic fixes where possible
|
|
32
|
+
plane: Explicit diagnostic plane — runtime_personal, installation_live, or database_real
|
|
31
33
|
"""
|
|
32
34
|
report = DoctorReport(overall_status="healthy")
|
|
33
35
|
start = time.monotonic()
|
|
@@ -44,6 +46,13 @@ def run_doctor(tier: str = "boot", fix: bool = False) -> DoctorReport:
|
|
|
44
46
|
report.duration_ms = int((time.monotonic() - start) * 1000)
|
|
45
47
|
return report
|
|
46
48
|
|
|
49
|
+
_, preflight = diagnostic_plane_preflight(plane)
|
|
50
|
+
if preflight is not None:
|
|
51
|
+
report.add(preflight)
|
|
52
|
+
report.compute_status()
|
|
53
|
+
report.duration_ms = int((time.monotonic() - start) * 1000)
|
|
54
|
+
return report
|
|
55
|
+
|
|
47
56
|
tiers = _TIER_ORDER if tier == "all" else [tier]
|
|
48
57
|
|
|
49
58
|
for t in tiers:
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Diagnostic plane preflight for NEXO Doctor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from doctor.models import DoctorCheck
|
|
6
|
+
|
|
7
|
+
VALID_DIAGNOSTIC_PLANES = {
|
|
8
|
+
"product_public": {
|
|
9
|
+
"label": "producto público",
|
|
10
|
+
"use": "release contracts, artefactos publicados, compare/, docs y surfaces públicas del repo",
|
|
11
|
+
},
|
|
12
|
+
"runtime_personal": {
|
|
13
|
+
"label": "runtime personal",
|
|
14
|
+
"use": "~/.nexo, scripts personales, followups, reminders y hábitos operativos del operador",
|
|
15
|
+
},
|
|
16
|
+
"installation_live": {
|
|
17
|
+
"label": "instalación viva",
|
|
18
|
+
"use": "runtime instalado, hooks activos, clientes conectados, cron sync y parity de la instalación local",
|
|
19
|
+
},
|
|
20
|
+
"database_real": {
|
|
21
|
+
"label": "BD real",
|
|
22
|
+
"use": "SQLite/MySQL reales, filas, schema, deudas, sesiones y evidencia persistida",
|
|
23
|
+
},
|
|
24
|
+
"cooperator": {
|
|
25
|
+
"label": "co-operador",
|
|
26
|
+
"use": "comportamiento del agente, protocolo, comunicación y decisiones del asistente",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
DOCTOR_COMPATIBLE_PLANES = {"runtime_personal", "installation_live", "database_real"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize_diagnostic_plane(plane: str = "") -> str:
|
|
34
|
+
clean = (plane or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
35
|
+
return clean if clean in VALID_DIAGNOSTIC_PLANES else ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def diagnostic_plane_choices() -> list[str]:
|
|
39
|
+
return sorted(VALID_DIAGNOSTIC_PLANES)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def diagnostic_plane_preflight(plane: str = "") -> tuple[str, DoctorCheck | None]:
|
|
43
|
+
clean_plane = normalize_diagnostic_plane(plane)
|
|
44
|
+
if not clean_plane:
|
|
45
|
+
options = ", ".join(diagnostic_plane_choices())
|
|
46
|
+
return "", DoctorCheck(
|
|
47
|
+
id="orchestrator.diagnostic_plane_required",
|
|
48
|
+
tier="orchestrator",
|
|
49
|
+
status="critical",
|
|
50
|
+
severity="error",
|
|
51
|
+
summary="El diagnóstico está bloqueado hasta fijar explícitamente el plano",
|
|
52
|
+
evidence=[
|
|
53
|
+
f"planes válidos: {options}",
|
|
54
|
+
"Usa `runtime_personal` para ~/.nexo y hábitos del runtime; `installation_live` para hooks/clientes/instalación; `database_real` para filas y schema reales.",
|
|
55
|
+
],
|
|
56
|
+
repair_plan=[
|
|
57
|
+
"Repite `nexo_doctor` o `nexo doctor` con `plane='runtime_personal'`, `plane='installation_live'` o `plane='database_real'`.",
|
|
58
|
+
"Si el problema pertenece a producto público o al co-operador, usa el surface correcto en vez de NEXO Doctor.",
|
|
59
|
+
],
|
|
60
|
+
escalation_prompt=(
|
|
61
|
+
"NEXO mezcló planos en diagnósticos anteriores. El doctor no debe correr hasta que se elija "
|
|
62
|
+
"explícitamente si el problema está en producto público, runtime personal, instalación viva, BD real o co-operador."
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if clean_plane not in DOCTOR_COMPATIBLE_PLANES:
|
|
67
|
+
plane_info = VALID_DIAGNOSTIC_PLANES[clean_plane]
|
|
68
|
+
return clean_plane, DoctorCheck(
|
|
69
|
+
id="orchestrator.diagnostic_plane_mismatch",
|
|
70
|
+
tier="orchestrator",
|
|
71
|
+
status="degraded",
|
|
72
|
+
severity="warn",
|
|
73
|
+
summary=f"NEXO Doctor no es la superficie correcta para el plano {plane_info['label']}",
|
|
74
|
+
evidence=[
|
|
75
|
+
f"plane: {clean_plane}",
|
|
76
|
+
f"este plano se diagnostica mejor desde: {plane_info['use']}",
|
|
77
|
+
],
|
|
78
|
+
repair_plan=[
|
|
79
|
+
"Si quieres diagnosticar runtime/instalación/BD, vuelve a lanzar el doctor con el plano correcto.",
|
|
80
|
+
"Si el problema es del producto público o del co-operador, usa release checks, repo checks o herramientas de protocolo/sesión en vez de Doctor.",
|
|
81
|
+
],
|
|
82
|
+
escalation_prompt=(
|
|
83
|
+
"El plano elegido no corresponde al runtime doctor. Cambia de plano o de herramienta antes de seguir para no mezclar diagnóstico técnico con producto o comportamiento del agente."
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return clean_plane, None
|