nexo-brain 7.13.6 → 7.13.8

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": "7.13.6",
3
+ "version": "7.13.8",
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,11 @@
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 `7.13.6` is the current packaged-runtime line. Patch release over v7.13.5Codex hook sync now renders the managed `PreToolUse` shell/exec_command guard with native Windows `cmd.exe` syntax while preserving the existing POSIX command on macOS/Linux. Result: coordinated Desktop bundles can ship the fixed Brain without changing the Mac/Windows installation contract.
21
+ Version `7.13.8` is the current packaged-runtime line. Patch release over v7.13.7Brain now rejects Python <3.10 during Desktop-managed fresh installs, honors the Python interpreter prepared by Desktop, and fails clearly before dependency resolution if an unsupported Apple Python 3.9 reaches the installer.
22
+
23
+ Previously in `7.13.7`: patch release — Brain adds an authenticated official protocol-card client (`nexo_card_catalog`, `nexo_card_get`, `nexo_card_match`) so agents can ask the NEXO Desktop backend for the right task protocol at runtime. The protocol corpus stays private on the server; this open-source package ships only the client, tool map, and agent guidance.
24
+
25
+ Previously in `7.13.6`: patch release — Codex hook sync now renders the managed `PreToolUse` shell/exec_command guard with native Windows `cmd.exe` syntax while preserving the existing POSIX command on macOS/Linux. Result: coordinated Desktop bundles can ship the fixed Brain without changing the Mac/Windows installation contract.
22
26
 
23
27
  Previously in `7.13.3`: unified release — doctor now repairs orphan personal script metadata and ignores historical `versions/**` snapshots, `nexo update` prunes runtime snapshots older than two back, protocol compliance self-heals missing task-open/change-log/stale-session gaps, headless automation uses bounded timeouts, Guardian false positives are tightened, and Codex CLI config/default checks are release-gated. Result: coordinated Desktop bundles can ship the new Brain without changing the Mac/Windows installation contract.
24
28
 
package/bin/nexo-brain.js CHANGED
@@ -36,6 +36,8 @@ if (process.platform === "win32") {
36
36
  let NEXO_HOME = process.env.NEXO_HOME || path.join(require("os").homedir(), ".nexo");
37
37
  const DEFAULT_ASSISTANT_NAME = "Nova";
38
38
  const RESERVED_ASSISTANT_NAME_KEYS = new Set(["nexo", "nexobrain", "nexodesktop"]);
39
+ const MIN_INSTALLER_PYTHON_MAJOR = 3;
40
+ const MIN_INSTALLER_PYTHON_MINOR = 10;
39
41
 
40
42
  function normalizeAssistantNameCandidate(value) {
41
43
  return String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
@@ -225,6 +227,57 @@ function run(cmd, opts = {}) {
225
227
  }
226
228
  }
227
229
 
230
+ function shSingleQuote(value) {
231
+ return "'" + String(value || "").replace(/'/g, "'\\''") + "'";
232
+ }
233
+
234
+ function runPythonProbe(pythonBin, args, timeout = 15000) {
235
+ if (!pythonBin) return null;
236
+ try {
237
+ const result = spawnSync(pythonBin, args, {
238
+ encoding: "utf8",
239
+ stdio: ["ignore", "pipe", "pipe"],
240
+ timeout,
241
+ });
242
+ if (result.status !== 0) return null;
243
+ return String(result.stdout || result.stderr || "").trim();
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
249
+ function pythonVersion(pythonBin) {
250
+ return runPythonProbe(pythonBin, ["-c", "import sys; print(sys.version.split()[0])"]);
251
+ }
252
+
253
+ function pythonVersionMeetsMinimum(versionText) {
254
+ const match = String(versionText || "").trim().match(/^(\d+)\.(\d+)(?:\.|$)/);
255
+ if (!match) return false;
256
+ const major = Number(match[1]);
257
+ const minor = Number(match[2]);
258
+ return major > MIN_INSTALLER_PYTHON_MAJOR
259
+ || (major === MIN_INSTALLER_PYTHON_MAJOR && minor >= MIN_INSTALLER_PYTHON_MINOR);
260
+ }
261
+
262
+ function resolveInstallerPython() {
263
+ const candidates = [
264
+ process.env.NEXO_BOOTSTRAP_PYTHON,
265
+ process.env.NEXO_RUNTIME_PYTHON,
266
+ process.env.NEXO_PYTHON,
267
+ run("which python3"),
268
+ run("which python"),
269
+ ].filter(Boolean);
270
+ const seen = new Set();
271
+ for (const candidate of candidates) {
272
+ const clean = String(candidate || "").trim();
273
+ if (!clean || seen.has(clean)) continue;
274
+ seen.add(clean);
275
+ const version = pythonVersion(clean);
276
+ if (version && pythonVersionMeetsMinimum(version)) return clean;
277
+ }
278
+ return "";
279
+ }
280
+
228
281
  function findBundledWheel(wheelsDir, prefix) {
229
282
  try {
230
283
  const normalizedPrefix = String(prefix || "").toLowerCase() + "-";
@@ -1710,6 +1763,48 @@ function buildManagedCliEnv(extraEnv = {}) {
1710
1763
  };
1711
1764
  }
1712
1765
 
1766
+ function ensureDesktopNodeShim(desktopNode) {
1767
+ const clean = String(desktopNode || "").trim();
1768
+ if (!clean) return "";
1769
+ const shimDir = path.join(NEXO_HOME, "runtime", "bootstrap", "node-shim");
1770
+ fs.mkdirSync(shimDir, { recursive: true });
1771
+ if (process.platform === "win32") {
1772
+ const shimPath = path.join(shimDir, "node.cmd");
1773
+ fs.writeFileSync(
1774
+ shimPath,
1775
+ `@echo off\r\nset ELECTRON_RUN_AS_NODE=1\r\n"${clean}" %*\r\n`,
1776
+ );
1777
+ return shimDir;
1778
+ }
1779
+ const shimPath = path.join(shimDir, "node");
1780
+ fs.writeFileSync(
1781
+ shimPath,
1782
+ [
1783
+ "#!/bin/sh",
1784
+ "export ELECTRON_RUN_AS_NODE=1",
1785
+ `exec ${shSingleQuote(clean)} "$@"`,
1786
+ "",
1787
+ ].join("\n"),
1788
+ );
1789
+ fs.chmodSync(shimPath, 0o755);
1790
+ return shimDir;
1791
+ }
1792
+
1793
+ function withDesktopNodeShim(env, desktopNode) {
1794
+ try {
1795
+ const shimDir = ensureDesktopNodeShim(desktopNode);
1796
+ if (!shimDir) return env;
1797
+ return {
1798
+ ...env,
1799
+ ELECTRON_RUN_AS_NODE: "1",
1800
+ PATH: [shimDir, env.PATH || ""].filter(Boolean).join(path.delimiter),
1801
+ };
1802
+ } catch (err) {
1803
+ log(`Desktop Node shim could not be created: ${String(err && err.message || err)}`);
1804
+ return env;
1805
+ }
1806
+ }
1807
+
1713
1808
  function resolveManagedClaudeBinary() {
1714
1809
  const prefix = managedClaudePrefix();
1715
1810
  const candidates = process.platform === "win32"
@@ -1921,11 +2016,13 @@ function installClaudeCodeCli(platform) {
1921
2016
  return { installed: true, path: claudeInstalled };
1922
2017
  }
1923
2018
 
1924
- const installEnv = buildManagedCliEnv();
1925
2019
  const desktopNode = String(process.env.NEXO_DESKTOP_NODE || "").trim();
1926
2020
  const bundledNpmCli = String(process.env.NEXO_DESKTOP_NPM_CLI || "").trim();
1927
2021
  const managedPrefix = managedClaudePrefix();
1928
2022
  const desktopManaged = isDesktopManagedInstall();
2023
+ const npmViaDesktop = desktopNode && bundledNpmCli;
2024
+ let installEnv = buildManagedCliEnv();
2025
+ if (desktopNode) installEnv = withDesktopNodeShim(installEnv, desktopNode);
1929
2026
 
1930
2027
  // OFFLINE-FIRST v0.32.4: install claude-code wrapper + ALL its native packs
1931
2028
  // from bundled tarballs. Path: resources/brain-bundle/claude-code/*.tgz.
@@ -1963,8 +2060,18 @@ function installClaudeCodeCli(platform) {
1963
2060
  const tgzPaths = [path.join(bundledClaudeDir, wrapper), ...nativePacks.map((p) => path.join(bundledClaudeDir, p))];
1964
2061
  log(" Installing claude-code from bundled tarballs (offline, " + (1 + nativePacks.length) + " packs)...");
1965
2062
  spawnSync(
1966
- "npm",
1967
- ["install", "-g", "--prefix", managedPrefix, "--offline", "--no-audit", "--no-fund", ...tgzPaths],
2063
+ npmViaDesktop ? desktopNode : "npm",
2064
+ [
2065
+ ...(npmViaDesktop ? [bundledNpmCli] : []),
2066
+ "install",
2067
+ "-g",
2068
+ "--prefix",
2069
+ managedPrefix,
2070
+ "--offline",
2071
+ "--no-audit",
2072
+ "--no-fund",
2073
+ ...tgzPaths,
2074
+ ],
1968
2075
  { stdio: "inherit", env: installEnv },
1969
2076
  );
1970
2077
  claudeInstalled = detectInstalledClients().claude_code.path || "";
@@ -1977,8 +2084,15 @@ function installClaudeCodeCli(platform) {
1977
2084
  const tgzPath = path.join(bundledClaudeDir, wrapper);
1978
2085
  log(" Installing claude-code from bundled wrapper only (legacy bundle, may need network for native pack)...");
1979
2086
  spawnSync(
1980
- "npm",
1981
- ["install", "-g", "--prefix", managedPrefix, tgzPath],
2087
+ npmViaDesktop ? desktopNode : "npm",
2088
+ [
2089
+ ...(npmViaDesktop ? [bundledNpmCli] : []),
2090
+ "install",
2091
+ "-g",
2092
+ "--prefix",
2093
+ managedPrefix,
2094
+ tgzPath,
2095
+ ],
1982
2096
  { stdio: "inherit", env: installEnv },
1983
2097
  );
1984
2098
  claudeInstalled = detectInstalledClients().claude_code.path || "";
@@ -3081,7 +3195,7 @@ async function runSetup() {
3081
3195
  }
3082
3196
 
3083
3197
  // Find or install Python (platform-aware)
3084
- let python = run("which python3");
3198
+ let python = resolveInstallerPython();
3085
3199
  if (!python) {
3086
3200
  if (platform === "darwin") {
3087
3201
  // v0.32.5 — Mac vanilla NO trae python3. La auto-instalación de
@@ -3122,7 +3236,7 @@ async function runSetup() {
3122
3236
  // fallan al import. Pinning a `python@3.12` evita el drift.
3123
3237
  log("Python 3.12 not found. Installing via Homebrew...");
3124
3238
  spawnSync("brew", ["install", "python@3.12"], { stdio: "inherit" });
3125
- python = run("which python3.12") || run("which python3");
3239
+ python = resolveInstallerPython() || run("which python3.12") || run("which python3");
3126
3240
  }
3127
3241
  } else if (platform === "linux") {
3128
3242
  // Linux: try apt or yum
@@ -3132,7 +3246,7 @@ async function runSetup() {
3132
3246
  } else if (run("which yum")) {
3133
3247
  spawnSync("sudo", ["yum", "install", "-y", "python3", "python3-pip"], { stdio: "inherit" });
3134
3248
  }
3135
- python = run("which python3");
3249
+ python = resolveInstallerPython();
3136
3250
  }
3137
3251
  if (!python) {
3138
3252
  log("Python 3 not found and couldn't install automatically.");
@@ -3140,7 +3254,13 @@ async function runSetup() {
3140
3254
  process.exit(1);
3141
3255
  }
3142
3256
  }
3143
- const pyVersion = run(`${python} --version`);
3257
+ const pyVersion = pythonVersion(python);
3258
+ if (!pyVersion || !pythonVersionMeetsMinimum(pyVersion)) {
3259
+ log(pyVersion
3260
+ ? `Python at ${python} is ${pyVersion}; NEXO Brain requires Python >=${MIN_INSTALLER_PYTHON_MAJOR}.${MIN_INSTALLER_PYTHON_MINOR}.`
3261
+ : `Python at ${python || "(not found)"} is not executable.`);
3262
+ process.exit(1);
3263
+ }
3144
3264
  log(`Found ${pyVersion} at ${python}`);
3145
3265
  logMacPermissionsNotice(NEXO_HOME, python);
3146
3266
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.13.6",
3
+ "version": "7.13.8",
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",
@@ -0,0 +1,221 @@
1
+ """Authenticated client for NEXO official protocol cards.
2
+
3
+ The protocol corpus lives on the private NEXO Desktop backend. This open-source
4
+ plugin only knows how to authenticate and fetch cards at runtime.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import platform
12
+ import urllib.error
13
+ import urllib.parse
14
+ import urllib.request
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ DEFAULT_API_BASE = "https://nexo-desktop.com"
20
+ SHARED_AUTH_DIRNAME = "nexo-shared-auth"
21
+ SHARED_AUTH_FILENAME = "session.json"
22
+
23
+ _urlopen = urllib.request.urlopen
24
+
25
+
26
+ def _json(data: dict[str, Any]) -> str:
27
+ return json.dumps(data, ensure_ascii=False, indent=2)
28
+
29
+
30
+ def _as_bool(value: Any, default: bool = False) -> bool:
31
+ if isinstance(value, bool):
32
+ return value
33
+ if value is None:
34
+ return default
35
+ return str(value).strip().lower() in {"1", "true", "yes", "y", "si", "sí", "on"}
36
+
37
+
38
+ def _as_int(value: Any, default: int = 5) -> int:
39
+ try:
40
+ return int(value)
41
+ except Exception:
42
+ return default
43
+
44
+
45
+ def _api_base() -> str:
46
+ raw = os.environ.get("NEXO_DESKTOP_API_BASE") or os.environ.get("NEXO_CARDS_API_BASE") or DEFAULT_API_BASE
47
+ return raw.strip().rstrip("/") or DEFAULT_API_BASE
48
+
49
+
50
+ def _normalize_locale(locale: str = "es") -> str:
51
+ return "en" if str(locale or "").lower().startswith("en") else "es"
52
+
53
+
54
+ def _shared_auth_candidates() -> list[Path]:
55
+ candidates: list[Path] = []
56
+ for env_name in ("NEXO_SHARED_AUTH_FILE", "NEXO_DESKTOP_SHARED_AUTH_FILE"):
57
+ raw = os.environ.get(env_name)
58
+ if raw:
59
+ candidates.append(Path(raw).expanduser())
60
+
61
+ appdata = os.environ.get("APPDATA")
62
+ if appdata:
63
+ candidates.append(Path(appdata) / SHARED_AUTH_DIRNAME / SHARED_AUTH_FILENAME)
64
+
65
+ home = Path.home()
66
+ system = platform.system().lower()
67
+ if system == "darwin":
68
+ candidates.append(home / "Library" / "Application Support" / SHARED_AUTH_DIRNAME / SHARED_AUTH_FILENAME)
69
+ elif system == "windows":
70
+ candidates.append(home / "AppData" / "Roaming" / SHARED_AUTH_DIRNAME / SHARED_AUTH_FILENAME)
71
+ else:
72
+ xdg = os.environ.get("XDG_CONFIG_HOME")
73
+ if xdg:
74
+ candidates.append(Path(xdg) / SHARED_AUTH_DIRNAME / SHARED_AUTH_FILENAME)
75
+ candidates.append(home / ".config" / SHARED_AUTH_DIRNAME / SHARED_AUTH_FILENAME)
76
+
77
+ nexo_home = Path(os.environ.get("NEXO_HOME", str(home / ".nexo"))).expanduser()
78
+ candidates.append(nexo_home / "runtime" / "shared-auth" / SHARED_AUTH_FILENAME)
79
+ return candidates
80
+
81
+
82
+ def _read_token_from_shared_auth() -> str:
83
+ for path in _shared_auth_candidates():
84
+ try:
85
+ if not path.is_file():
86
+ continue
87
+ payload = json.loads(path.read_text(encoding="utf-8"))
88
+ token = str(payload.get("token") or "").strip()
89
+ if token:
90
+ return token
91
+ except Exception:
92
+ continue
93
+ return ""
94
+
95
+
96
+ def _read_token() -> str:
97
+ for env_name in ("NEXO_DESKTOP_AUTH_TOKEN", "NEXO_CARDS_TOKEN", "NEXO_AUTH_TOKEN"):
98
+ token = str(os.environ.get(env_name) or "").strip()
99
+ if token:
100
+ return token
101
+ return _read_token_from_shared_auth()
102
+
103
+
104
+ def _error(error_type: str, message: str, *, status: int = 0, extra: dict[str, Any] | None = None) -> dict[str, Any]:
105
+ payload: dict[str, Any] = {
106
+ "ok": False,
107
+ "error": {
108
+ "type": error_type,
109
+ "message": message,
110
+ },
111
+ }
112
+ if status:
113
+ payload["status"] = status
114
+ if extra:
115
+ payload["error"].update(extra)
116
+ return payload
117
+
118
+
119
+ def _decode_body(raw: bytes) -> dict[str, Any]:
120
+ if not raw:
121
+ return {}
122
+ try:
123
+ decoded = json.loads(raw.decode("utf-8"))
124
+ return decoded if isinstance(decoded, dict) else {"data": decoded}
125
+ except Exception:
126
+ return {"raw": raw.decode("utf-8", errors="replace")[:1000]}
127
+
128
+
129
+ def _request_json(method: str, path: str, *, body: dict[str, Any] | None = None, locale: str = "es") -> dict[str, Any]:
130
+ token = _read_token()
131
+ if not token:
132
+ return _error(
133
+ "not_authenticated",
134
+ "No hay sesión NEXO Desktop disponible para consultar fichas.",
135
+ )
136
+
137
+ data = json.dumps(body or {}, ensure_ascii=False).encode("utf-8") if body is not None else None
138
+ request = urllib.request.Request(
139
+ f"{_api_base()}{path}",
140
+ data=data,
141
+ method=method.upper(),
142
+ headers={
143
+ "Accept": "application/json",
144
+ "Accept-Language": _normalize_locale(locale),
145
+ "Authorization": f"Bearer {token}",
146
+ **({"Content-Type": "application/json"} if data is not None else {}),
147
+ },
148
+ )
149
+ try:
150
+ with _urlopen(request, timeout=20) as response:
151
+ payload = _decode_body(response.read())
152
+ if isinstance(payload, dict):
153
+ payload.setdefault("ok", True)
154
+ payload.setdefault("status", getattr(response, "status", 200))
155
+ return payload
156
+ return {"ok": True, "data": payload, "status": getattr(response, "status", 200)}
157
+ except urllib.error.HTTPError as exc:
158
+ payload = _decode_body(exc.read())
159
+ error = payload.get("error") if isinstance(payload, dict) else None
160
+ if isinstance(error, dict):
161
+ return _error(
162
+ str(error.get("type") or "request_failed"),
163
+ str(error.get("message") or f"HTTP {exc.code}"),
164
+ status=int(exc.code or 0),
165
+ extra={k: v for k, v in error.items() if k not in {"type", "message"}},
166
+ )
167
+ return _error("request_failed", f"Protocol cards API returned HTTP {exc.code}.", status=int(exc.code or 0))
168
+ except Exception as exc:
169
+ return _error("network_error", str(exc))
170
+
171
+
172
+ def handle_card_catalog(locale: str = "es") -> str:
173
+ """Return the visible official protocol card catalog. Never includes protocols."""
174
+ params = urllib.parse.urlencode({"locale": _normalize_locale(locale)})
175
+ return _json(_request_json("GET", f"/api/cards/catalog?{params}", locale=locale))
176
+
177
+
178
+ def handle_card_get(slug: str, locale: str = "es") -> str:
179
+ """Fetch one official protocol card, including protocol text, by slug."""
180
+ clean_slug = str(slug or "").strip()
181
+ if not clean_slug:
182
+ return _json(_error("invalid_input", "slug is required"))
183
+ params = urllib.parse.urlencode({"locale": _normalize_locale(locale)})
184
+ safe_slug = urllib.parse.quote(clean_slug, safe="")
185
+ return _json(_request_json("GET", f"/api/cards/{safe_slug}?{params}", locale=locale))
186
+
187
+
188
+ def handle_card_match(
189
+ query: str,
190
+ limit: int = 5,
191
+ include_protocol: bool = True,
192
+ locale: str = "es",
193
+ category: str = "",
194
+ business_type: str = "",
195
+ ) -> str:
196
+ """Find official protocol cards for a user request.
197
+
198
+ Use this before non-trivial work when available. Protocols are fetched from
199
+ the authenticated backend at runtime; this package does not embed them.
200
+ """
201
+ clean_query = str(query or "").strip()
202
+ if not clean_query:
203
+ return _json(_error("invalid_input", "query is required"))
204
+ body = {
205
+ "query": clean_query,
206
+ "limit": max(1, min(20, _as_int(limit, 5))),
207
+ "include_protocol": _as_bool(include_protocol, True),
208
+ "locale": _normalize_locale(locale),
209
+ }
210
+ if category:
211
+ body["category"] = str(category).strip()
212
+ if business_type:
213
+ body["business_type"] = str(business_type).strip()
214
+ return _json(_request_json("POST", "/api/cards/match", body=body, locale=locale))
215
+
216
+
217
+ TOOLS = [
218
+ (handle_card_catalog, "nexo_card_catalog", "List visible official NEXO protocol cards from the authenticated backend. Does not return protocol text."),
219
+ (handle_card_get, "nexo_card_get", "Fetch one official NEXO protocol card by slug from the authenticated backend, including protocol text."),
220
+ (handle_card_match, "nexo_card_match", "Find the official NEXO protocol card for a user request. Call before non-trivial work when available."),
221
+ ]
@@ -30,13 +30,14 @@ Claude Code may list `mcp__nexo__*` tools as **deferred** at session start (name
30
30
  - Diagnostic plane: `nexo_doctor plane='installation_live'` inspects client/install surfaces — consult it when tools appear missing on a fresh install.
31
31
  <!-- nexo:end:tools_at_startup -->
32
32
 
33
- ## Protocol (6 rules)
33
+ ## Protocol (7 rules)
34
34
  1. `nexo_startup` once per session and keep the returned `SID`.
35
35
  2. `nexo_heartbeat` on every user message.
36
36
  3. `nexo_task_open` for any non-trivial work. For `edit` / `execute` / `delegate`, this is the default path. If the work is long multi-step or likely to cross messages/sessions, also open `nexo_workflow_open` and keep it updated. If task_open surfaces blocking conditioned-file learnings, review them and acknowledge guard before any write / delete step.
37
- 4. Do not say `done`, `fixed`, or `sent` without evidence captured in `nexo_task_close`.
38
- 5. If a correction revealed a reusable pattern, capture or supersede the learning immediately so contradictory rules do not remain active.
39
- 6. On clear end-of-session intent, write the diary before replying.
37
+ 4. If `nexo_card_match` is available, call it before non-trivial user-facing creation/analysis work. If it returns an official protocol, follow it silently; if unavailable or no match, continue normally.
38
+ 5. Do not say `done`, `fixed`, or `sent` without evidence captured in `nexo_task_close`.
39
+ 6. If a correction revealed a reusable pattern, capture or supersede the learning immediately so contradictory rules do not remain active.
40
+ 7. On clear end-of-session intent, write the diary before replying.
40
41
 
41
42
  ## Response Pacing
42
43
  - After the first relevant tool or artifact result, reply with the answer immediately instead of silently chaining more investigation.
@@ -25,13 +25,14 @@ Codex (and Claude Code) may list `mcp__nexo__*` tools as **deferred** at session
25
25
  - If discovery still cannot resolve a `nexo_*` tool, then (and only then) treat it as a real runtime gap and surface it as a blocker.
26
26
  - Diagnostic plane: `nexo_doctor plane='installation_live'` inspects client/install surfaces — consult it when tools appear missing on a fresh install.
27
27
 
28
- ## Protocol (6 rules)
28
+ ## Protocol (7 rules)
29
29
  1. `nexo_startup` once per session, then keep the returned `SID`.
30
30
  2. `nexo_heartbeat` on every user message.
31
31
  3. `nexo_task_open` for any non-trivial work. For `edit` / `execute` / `delegate`, this is the default path. If the work is long multi-step or likely to cross messages/sessions, also open `nexo_workflow_open` and keep it updated. If task_open surfaces blocking conditioned-file learnings, review them and acknowledge guard before any write / delete step.
32
- 4. Do not say `done`, `fixed`, or `sent` without evidence captured in `nexo_task_close`.
33
- 5. If a correction revealed a reusable pattern, capture or supersede the learning immediately so contradictory rules do not remain active.
34
- 6. On clear end-of-session intent, write the diary before replying.
32
+ 4. If `nexo_card_match` is available, call it before non-trivial user-facing creation/analysis work. If it returns an official protocol, follow it silently; if unavailable or no match, continue normally.
33
+ 5. Do not say `done`, `fixed`, or `sent` without evidence captured in `nexo_task_close`.
34
+ 6. If a correction revealed a reusable pattern, capture or supersede the learning immediately so contradictory rules do not remain active.
35
+ 7. On clear end-of-session intent, write the diary before replying.
35
36
 
36
37
  ## Response Pacing
37
38
  - After the first relevant tool or artifact result, answer immediately instead of silently chaining more investigation.
@@ -7,6 +7,7 @@
7
7
  - **Diagnostic plane (MANDATORY before diagnosing NEXO):** fix the plane explicitly first — `product_public`, `runtime_personal`, `installation_live`, `database_real`, or `cooperator`. Do not mix product, runtime, install, DB, and agent-behavior explanations in the same diagnosis.
8
8
  - **Guard (MANDATORY before ANY code edit):** `nexo_guard_check(files='...', area='...')` BEFORE editing code. No exceptions. Blocking rules→resolve first. `nexo_track(sid=SID, paths=[...])` before shared files
9
9
  - **Skills (MANDATORY before multi-step tasks):** `nexo_skill_match(task)` to find reusable procedures. If match found, read it and follow the steps. After completion, `nexo_skill_result(id, success, context)` to record outcome.
10
+ - **Official protocol cards (MANDATORY when available):** before non-trivial user-facing creation/analysis work, call `nexo_card_match(query=...)`. If it returns a protocol, follow it silently. The protocol text is served by the authenticated backend and is not embedded in Brain.
10
11
  - **Learnings (MANDATORY on corrections):** When you discover a bug, pattern, or get corrected→`nexo_learning_add` IMMEDIATELY. Do NOT batch. Do NOT wait until end of session.
11
12
 
12
13
  ## Rules
@@ -315,6 +315,55 @@
315
315
  },
316
316
  "triggers_after": []
317
317
  },
318
+ "nexo_card_catalog": {
319
+ "description": "List visible official protocol cards from backend",
320
+ "category": "protocol_cards",
321
+ "source": "plugin:cards",
322
+ "requires": [],
323
+ "provides": [
324
+ "official_protocol_card_catalog"
325
+ ],
326
+ "internal_calls": [],
327
+ "enforcement": {
328
+ "level": "none",
329
+ "rules": []
330
+ },
331
+ "triggers_after": []
332
+ },
333
+ "nexo_card_get": {
334
+ "description": "Fetch one official protocol card from backend",
335
+ "category": "protocol_cards",
336
+ "source": "plugin:cards",
337
+ "requires": [
338
+ "desktop_auth_token"
339
+ ],
340
+ "provides": [
341
+ "official_protocol_card"
342
+ ],
343
+ "internal_calls": [],
344
+ "enforcement": {
345
+ "level": "none",
346
+ "rules": []
347
+ },
348
+ "triggers_after": []
349
+ },
350
+ "nexo_card_match": {
351
+ "description": "Match user request to an official protocol card from backend",
352
+ "category": "protocol_cards",
353
+ "source": "plugin:cards",
354
+ "requires": [
355
+ "desktop_auth_token"
356
+ ],
357
+ "provides": [
358
+ "official_protocol_card_match"
359
+ ],
360
+ "internal_calls": [],
361
+ "enforcement": {
362
+ "level": "none",
363
+ "rules": []
364
+ },
365
+ "triggers_after": []
366
+ },
318
367
  "nexo_change_commit": {
319
368
  "description": "Link change log entry to git commit",
320
369
  "category": "change_log",