nexo-brain 5.3.10 → 5.3.11

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.11",
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.3.10",
3
+ "version": "5.3.11",
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",
@@ -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 = ?,
@@ -22,6 +22,8 @@ import time
22
22
  from datetime import datetime, timedelta
23
23
  from pathlib import Path
24
24
 
25
+ from db import VALID_IMPACT_LEVELS, VALID_TASK_TYPES, validate_impact_level, validate_task_type
26
+
25
27
 
26
28
  def _get_db():
27
29
  from db import get_db
@@ -734,9 +736,19 @@ def handle_cortex_check(
734
736
  Returns:
735
737
  Mode (ask/propose/act), available tools, warnings, and relevant Core Rules
736
738
  """
739
+ try:
740
+ clean_type = validate_task_type(task_type)
741
+ except ValueError as exc:
742
+ return "\n".join(
743
+ [
744
+ f"ERROR: {exc}",
745
+ f"Valid task types: {', '.join(sorted(VALID_TASK_TYPES))}",
746
+ ]
747
+ )
748
+
737
749
  state = {
738
750
  "goal": goal.strip() if goal else "",
739
- "task_type": task_type if task_type in ("answer", "analyze", "edit", "execute", "delegate") else "answer",
751
+ "task_type": clean_type,
740
752
  "plan": _parse_json_list(plan),
741
753
  "known_facts": _parse_json_list(known_facts),
742
754
  "unknowns": _parse_json_list(unknowns),
@@ -860,8 +872,30 @@ def handle_cortex_decide(
860
872
  indent=2,
861
873
  )
862
874
 
863
- clean_type = task_type if task_type in {"answer", "analyze", "edit", "execute", "delegate"} else "execute"
864
- clean_level = impact_level if impact_level in {"medium", "high", "critical"} else "high"
875
+ try:
876
+ clean_type = validate_task_type(task_type)
877
+ except ValueError as exc:
878
+ return json.dumps(
879
+ {
880
+ "ok": False,
881
+ "error": str(exc),
882
+ "valid_task_types": sorted(VALID_TASK_TYPES),
883
+ },
884
+ ensure_ascii=False,
885
+ indent=2,
886
+ )
887
+ try:
888
+ clean_level = validate_impact_level(impact_level)
889
+ except ValueError as exc:
890
+ return json.dumps(
891
+ {
892
+ "ok": False,
893
+ "error": str(exc),
894
+ "valid_impact_levels": sorted(VALID_IMPACT_LEVELS),
895
+ },
896
+ ensure_ascii=False,
897
+ indent=2,
898
+ )
865
899
  parsed_constraints = _parse_json_list(constraints)
866
900
  parsed_evidence = _parse_json_list(evidence_refs)
867
901
  try:
@@ -10,6 +10,8 @@ import secrets
10
10
  import time
11
11
 
12
12
  from db import (
13
+ VALID_TASK_TYPES,
14
+ VALID_CLOSE_OUTCOMES,
13
15
  close_protocol_task,
14
16
  create_followup,
15
17
  latest_cortex_evaluation_for_task,
@@ -28,6 +30,8 @@ from db import (
28
30
  resolve_protocol_debts,
29
31
  search_learnings,
30
32
  task_has_cortex_evaluation,
33
+ validate_close_outcome,
34
+ validate_task_type,
31
35
  )
32
36
  from plugins.cortex import evaluate_cortex_state
33
37
  from plugins.guard import handle_guard_check
@@ -651,7 +655,18 @@ def handle_confidence_check(
651
655
  clean_goal = (goal or "").strip()
652
656
  if not clean_goal:
653
657
  return json.dumps({"ok": False, "error": "goal is required"}, ensure_ascii=False, indent=2)
654
- clean_type = task_type if task_type in {"answer", "analyze", "edit", "execute", "delegate"} else "answer"
658
+ try:
659
+ clean_type = validate_task_type(task_type)
660
+ except ValueError as exc:
661
+ return json.dumps(
662
+ {
663
+ "ok": False,
664
+ "error": str(exc),
665
+ "valid_task_types": sorted(VALID_TASK_TYPES),
666
+ },
667
+ ensure_ascii=False,
668
+ indent=2,
669
+ )
655
670
  result = evaluate_response_confidence(
656
671
  goal=clean_goal,
657
672
  task_type=clean_type,
@@ -693,7 +708,18 @@ def handle_task_open(
693
708
  if not clean_goal:
694
709
  return json.dumps({"ok": False, "error": "goal is required"}, ensure_ascii=False, indent=2)
695
710
 
696
- clean_type = task_type if task_type in {"answer", "analyze", "edit", "execute", "delegate"} else "answer"
711
+ try:
712
+ clean_type = validate_task_type(task_type)
713
+ except ValueError as exc:
714
+ return json.dumps(
715
+ {
716
+ "ok": False,
717
+ "error": str(exc),
718
+ "valid_task_types": sorted(VALID_TASK_TYPES),
719
+ },
720
+ ensure_ascii=False,
721
+ indent=2,
722
+ )
697
723
  files_list = _parse_list(files)
698
724
  protocol_strictness = get_protocol_strictness()
699
725
  if protocol_strictness in {"strict", "learning"} and clean_type == "edit" and not files_list:
@@ -949,7 +975,19 @@ def handle_task_close(
949
975
  indent=2,
950
976
  )
951
977
 
952
- clean_outcome = outcome if outcome in {"done", "partial", "blocked", "failed", "cancelled"} else "failed"
978
+ try:
979
+ clean_outcome = validate_close_outcome(outcome)
980
+ except ValueError as exc:
981
+ return json.dumps(
982
+ {
983
+ "ok": False,
984
+ "error": str(exc),
985
+ "task_id": task_id,
986
+ "valid_outcomes": sorted(VALID_CLOSE_OUTCOMES),
987
+ },
988
+ ensure_ascii=False,
989
+ indent=2,
990
+ )
953
991
  clean_evidence = (evidence or "").strip()
954
992
  files_changed_list = _parse_list(files_changed)
955
993
  planned_files = _parse_list(task.get("files") or "[]")