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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.3.10",
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.10` is the current packaged-runtime line: packaged installs and updates now refresh `~/.nexo/package.json` from the published npm package so runtime metadata stops drifting after successful updates, `nexo doctor --tier deep` no longer flags a healthy runtime as degraded just because the first daily `self-audit-summary.json` has not been produced yet, weekly Evolution once again emits explicit dimension telemetry instead of leaving `nexo_evolution_status` blank, and daily synthesis only pulls startup/update summaries into the briefing when they are operationally actionable.
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` tightens 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.
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
- Recommended defaults:
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
- - **Recommended profiles:** Claude Code + `Opus 4.6 with 1M context`; Codex + `gpt-5.4` with `xhigh` reasoning if you prefer Codex as your terminal or automation backend.
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. The current recommended Codex profile is `gpt-5.4` with `xhigh` reasoning. 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.
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.10",
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",
@@ -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."
@@ -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 = _backup_dbs()
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
- DEFAULT_CODEX_MODEL = "gpt-5.4"
52
- DEFAULT_CODEX_REASONING_EFFORT = "xhigh"
53
- DEFAULT_FAST_MODEL = "gpt-5.4-mini"
54
- DEFAULT_FAST_REASONING_EFFORT = "medium"
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": CLIENT_CODEX,
264
- "model": DEFAULT_FAST_MODEL,
265
- "reasoning_effort": DEFAULT_FAST_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(
@@ -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": "claude-opus-4-6[1m]", "reasoning_effort": ""},
66
- "codex": {"model": "gpt-5.4", "reasoning_effort": "xhigh"},
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
 
@@ -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
@@ -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 if task_type in VALID_TASK_TYPES else "answer"
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 if impact_level in VALID_IMPACT_LEVELS else "high"
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 if outcome in VALID_OUTCOMES else "failed"
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