nexo-brain 3.0.1 → 3.1.0

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.
Files changed (37) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +82 -0
  3. package/community/launch/2026-04-v3-0-2/case-study-outreach.md +36 -0
  4. package/community/launch/2026-04-v3-0-2/devto-v3-0-2.md +91 -0
  5. package/community/launch/2026-04-v3-0-2/github-discussion-v3-0-2.md +58 -0
  6. package/community/launch/2026-04-v3-0-2/x-thread-v3-0-2.md +60 -0
  7. package/hooks/hooks.json +12 -0
  8. package/package.json +1 -1
  9. package/src/auto_update.py +23 -6
  10. package/src/client_sync.py +6 -0
  11. package/src/cognitive/_memory.py +14 -7
  12. package/src/cognitive/_search.py +12 -5
  13. package/src/crons/sync.py +2 -1
  14. package/src/doctor/models.py +25 -0
  15. package/src/doctor/orchestrator.py +32 -2
  16. package/src/doctor/providers/boot.py +7 -7
  17. package/src/doctor/providers/deep.py +24 -21
  18. package/src/doctor/providers/runtime.py +154 -137
  19. package/src/evolution_cycle.py +76 -47
  20. package/src/hook_guardrails.py +182 -0
  21. package/src/hooks/protocol-guardrail.sh +1 -1
  22. package/src/hooks/protocol-pretool-guardrail.sh +9 -0
  23. package/src/kg_populate.py +21 -19
  24. package/src/maintenance.py +3 -3
  25. package/src/migrate_embeddings.py +36 -34
  26. package/src/plugins/backup.py +24 -12
  27. package/src/plugins/protocol.py +15 -0
  28. package/src/plugins/schedule.py +13 -1
  29. package/src/plugins/update.py +18 -4
  30. package/src/protocol_settings.py +59 -0
  31. package/src/public_contribution.py +10 -14
  32. package/src/public_evolution_queue.py +241 -0
  33. package/src/scripts/nexo-catchup.py +15 -15
  34. package/src/scripts/nexo-daily-self-audit.py +677 -28
  35. package/src/scripts/nexo-evolution-run.py +44 -4
  36. package/src/server.py +26 -1
  37. package/src/state_watchers_runtime.py +42 -35
@@ -3,6 +3,7 @@
3
3
  import json
4
4
  import os
5
5
  import platform
6
+ import re
6
7
  import subprocess
7
8
  from pathlib import Path
8
9
 
@@ -69,9 +70,20 @@ def _summary_has_warning(summary: str = "") -> bool:
69
70
  lowered = str(summary or "").strip().lower()
70
71
  if not lowered:
71
72
  return False
73
+ if re.search(r"\b[1-9]\d*\s+(errors?|warnings?)\b", lowered):
74
+ return True
72
75
  if "no warning" in lowered or "without warnings" in lowered:
73
76
  return False
74
- warning_tokens = ("warning", "warnings", "warn:", "degraded", "partial failure", "issues detected")
77
+ warning_tokens = (
78
+ "warning",
79
+ "warnings",
80
+ "warn:",
81
+ "degraded",
82
+ "partial failure",
83
+ "issues detected",
84
+ "completed with findings",
85
+ "findings detected",
86
+ )
75
87
  return any(token in lowered for token in warning_tokens)
76
88
 
77
89
 
@@ -149,14 +149,21 @@ def _backup_databases() -> tuple[str, str | None]:
149
149
 
150
150
  for db_file in db_files:
151
151
  dest = backup_dir / db_file.name
152
+ src_conn = None
153
+ dst_conn = None
152
154
  try:
153
155
  src_conn = sqlite3.connect(str(db_file))
154
156
  dst_conn = sqlite3.connect(str(dest))
155
157
  src_conn.backup(dst_conn)
156
- dst_conn.close()
157
- src_conn.close()
158
158
  except Exception as e:
159
159
  return str(backup_dir), f"Failed to backup {db_file.name}: {e}"
160
+ finally:
161
+ for conn in (dst_conn, src_conn):
162
+ if conn is not None:
163
+ try:
164
+ conn.close()
165
+ except Exception:
166
+ pass
160
167
 
161
168
  return str(backup_dir), None
162
169
 
@@ -170,14 +177,21 @@ def _restore_databases(backup_dir: str):
170
177
  # Try to find original location
171
178
  for candidate in [DATA_DIR / db_backup.name, NEXO_HOME / db_backup.name, SRC_DIR / db_backup.name]:
172
179
  if candidate.is_file():
180
+ src_conn = None
181
+ dst_conn = None
173
182
  try:
174
183
  src_conn = sqlite3.connect(str(db_backup))
175
184
  dst_conn = sqlite3.connect(str(candidate))
176
185
  src_conn.backup(dst_conn)
177
- dst_conn.close()
178
- src_conn.close()
179
186
  except Exception:
180
187
  pass
188
+ finally:
189
+ for conn in (dst_conn, src_conn):
190
+ if conn is not None:
191
+ try:
192
+ conn.close()
193
+ except Exception:
194
+ pass
181
195
  break
182
196
 
183
197
 
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ """Shared protocol-discipline settings loaded from calibration.json."""
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+
10
+ DEFAULT_PROTOCOL_STRICTNESS = "lenient"
11
+ VALID_PROTOCOL_STRICTNESS = {"lenient", "strict", "learning"}
12
+
13
+
14
+ def _nexo_home() -> Path:
15
+ return Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))).expanduser()
16
+
17
+
18
+ def _calibration_path() -> Path:
19
+ return _nexo_home() / "brain" / "calibration.json"
20
+
21
+
22
+ def normalize_protocol_strictness(value: str | None) -> str:
23
+ candidate = str(value or "").strip().lower()
24
+ aliases = {
25
+ "default": "lenient",
26
+ "normal": "lenient",
27
+ "off": "lenient",
28
+ "warn": "lenient",
29
+ "soft": "lenient",
30
+ "hard": "strict",
31
+ "guided": "learning",
32
+ }
33
+ candidate = aliases.get(candidate, candidate)
34
+ if candidate in VALID_PROTOCOL_STRICTNESS:
35
+ return candidate
36
+ return DEFAULT_PROTOCOL_STRICTNESS
37
+
38
+
39
+ def get_protocol_strictness() -> str:
40
+ env_override = os.environ.get("NEXO_PROTOCOL_STRICTNESS", "").strip()
41
+ if env_override:
42
+ return normalize_protocol_strictness(env_override)
43
+
44
+ cal_path = _calibration_path()
45
+ if not cal_path.is_file():
46
+ return DEFAULT_PROTOCOL_STRICTNESS
47
+
48
+ try:
49
+ payload = json.loads(cal_path.read_text())
50
+ except Exception:
51
+ return DEFAULT_PROTOCOL_STRICTNESS
52
+
53
+ preferences = payload.get("preferences") if isinstance(payload, dict) else {}
54
+ candidate = ""
55
+ if isinstance(preferences, dict):
56
+ candidate = str(preferences.get("protocol_strictness", "") or "").strip()
57
+ if not candidate and isinstance(payload, dict):
58
+ candidate = str(payload.get("protocol_strictness", "") or "").strip()
59
+ return normalize_protocol_strictness(candidate)
@@ -14,7 +14,7 @@ import re
14
14
  import shutil
15
15
  import socket
16
16
  import subprocess
17
- from datetime import datetime, timedelta, timezone
17
+ from datetime import datetime, timezone
18
18
  from pathlib import Path
19
19
 
20
20
  from runtime_power import load_schedule_config, save_schedule_config
@@ -33,8 +33,6 @@ STATUS_PENDING_AUTH = "pending_auth"
33
33
  STATUS_PAUSED_OPEN_PR = "paused_open_pr"
34
34
  STATUS_COOLDOWN = "cooldown"
35
35
  STATUS_OFF = "off"
36
- COOLDOWN_HOURS_MERGED = 168
37
- COOLDOWN_HOURS_CLOSED = 72
38
36
  VALID_MODES = {MODE_UNSET, MODE_OFF, MODE_DRAFT_PRS, MODE_PENDING_AUTH}
39
37
  VALID_STATUSES = {
40
38
  STATUS_UNSET,
@@ -224,10 +222,6 @@ def _parse_iso(ts: str | None) -> datetime | None:
224
222
  return None
225
223
 
226
224
 
227
- def _future_iso(hours: int) -> str:
228
- return (_utcnow() + timedelta(hours=hours)).isoformat()
229
-
230
-
231
225
  def format_public_contribution_label(config: dict | None = None) -> str:
232
226
  cfg = normalize_public_contribution_config(config)
233
227
  if cfg["mode"] == MODE_DRAFT_PRS:
@@ -369,12 +363,13 @@ def refresh_public_contribution_state(config: dict | None = None) -> dict:
369
363
  config["status"] = STATUS_PAUSED_OPEN_PR
370
364
  save_public_contribution_config(config)
371
365
  return config
366
+ resolution = "merged" if payload.get("mergedAt") else "closed"
372
367
  config["active_pr_url"] = ""
373
368
  config["active_pr_number"] = None
374
369
  config["active_branch"] = ""
375
- cooldown_hours = COOLDOWN_HOURS_MERGED if payload.get("mergedAt") else COOLDOWN_HOURS_CLOSED
376
- config["cooldown_until"] = _future_iso(cooldown_hours)
377
- config["status"] = STATUS_COOLDOWN
370
+ config["cooldown_until"] = ""
371
+ config["status"] = STATUS_ACTIVE
372
+ config["last_result"] = f"resolved_pr:{resolution}:{payload.get('url') or ''}".rstrip(":")
378
373
  save_public_contribution_config(config)
379
374
  return config
380
375
  return _set_pending_auth(
@@ -384,7 +379,11 @@ def refresh_public_contribution_state(config: dict | None = None) -> dict:
384
379
 
385
380
  cooldown_until = _parse_iso(config.get("cooldown_until"))
386
381
  if cooldown_until and cooldown_until > _utcnow():
387
- config["status"] = STATUS_COOLDOWN
382
+ # Legacy installs used a post-merge/close cooldown that blocked the next
383
+ # public contribution cycle even after maintainers resolved the Draft PR.
384
+ # Public contribution should pause only while the PR is still open.
385
+ config["cooldown_until"] = ""
386
+ config["status"] = STATUS_ACTIVE
388
387
  save_public_contribution_config(config)
389
388
  return config
390
389
 
@@ -430,9 +429,6 @@ def can_run_public_contribution(config: dict | None = None) -> tuple[bool, str,
430
429
  return False, "public contribution is disabled", config
431
430
  if config["status"] == STATUS_PAUSED_OPEN_PR:
432
431
  return False, "an active Draft PR is already open for this machine", config
433
- cooldown_until = _parse_iso(config.get("cooldown_until"))
434
- if cooldown_until and cooldown_until > _utcnow():
435
- return False, f"cooldown until {cooldown_until.isoformat()}", config
436
432
  return True, "", config
437
433
 
438
434
 
@@ -0,0 +1,241 @@
1
+ from __future__ import annotations
2
+
3
+ """Durable queue for public-core ports discovered outside the public runner.
4
+
5
+ Managed flows such as self-audit may apply a local/core fix inline. When that
6
+ fix belongs in the public repository as well, we persist a normalized queue
7
+ entry in ``evolution_log`` so the weekly public contribution cycle can port it
8
+ later instead of losing the improvement inside one machine.
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import sqlite3
14
+ from pathlib import Path
15
+
16
+
17
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))).expanduser()
18
+ QUEUE_CLASSIFICATION = "public_port_queue"
19
+ QUEUE_STATUS_PENDING = "pending_public_port"
20
+ PUBLIC_ALLOWED_PREFIXES = (
21
+ "src/",
22
+ "bin/",
23
+ "tests/",
24
+ "templates/",
25
+ "hooks/",
26
+ "migrations/",
27
+ ".claude-plugin/",
28
+ )
29
+
30
+
31
+ def resolve_repo_root(nexo_code: str | os.PathLike[str] | None = None) -> Path | None:
32
+ raw = Path(
33
+ nexo_code
34
+ or os.environ.get("NEXO_CODE")
35
+ or str(NEXO_HOME)
36
+ ).expanduser()
37
+ candidates = []
38
+ if raw.name == "src":
39
+ candidates.append(raw.parent)
40
+ candidates.append(raw)
41
+ for candidate in candidates:
42
+ if (candidate / "package.json").exists():
43
+ return candidate.resolve()
44
+ return None
45
+
46
+
47
+ def normalize_public_path(
48
+ filepath: str,
49
+ *,
50
+ repo_root: Path | None = None,
51
+ ) -> str:
52
+ text = str(filepath or "").strip()
53
+ if not text:
54
+ return ""
55
+
56
+ normalized_raw = text.replace("\\", "/").lstrip("./")
57
+ if any(
58
+ normalized_raw == prefix.rstrip("/")
59
+ or normalized_raw.startswith(prefix)
60
+ for prefix in PUBLIC_ALLOWED_PREFIXES
61
+ ):
62
+ return normalized_raw
63
+
64
+ repo_root = repo_root or resolve_repo_root()
65
+ if not repo_root:
66
+ return ""
67
+
68
+ candidate = Path(text).expanduser()
69
+ if not candidate.is_absolute():
70
+ candidate = repo_root / candidate
71
+ try:
72
+ rel = candidate.resolve().relative_to(repo_root.resolve()).as_posix()
73
+ except Exception:
74
+ for prefix in PUBLIC_ALLOWED_PREFIXES:
75
+ marker = normalized_raw.find(prefix)
76
+ if marker >= 0:
77
+ return normalized_raw[marker:]
78
+ return ""
79
+ if any(rel == prefix.rstrip("/") or rel.startswith(prefix) for prefix in PUBLIC_ALLOWED_PREFIXES):
80
+ return rel
81
+ return ""
82
+
83
+
84
+ def is_public_core_path(filepath: str, *, repo_root: Path | None = None) -> bool:
85
+ return bool(normalize_public_path(filepath, repo_root=repo_root))
86
+
87
+
88
+ def queue_public_port_candidate(
89
+ conn: sqlite3.Connection,
90
+ *,
91
+ title: str,
92
+ reasoning: str,
93
+ files_changed: list[str],
94
+ source: str,
95
+ metadata: dict | None = None,
96
+ ) -> dict:
97
+ row = conn.execute(
98
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='evolution_log'"
99
+ ).fetchone()
100
+ if not row:
101
+ return {"ok": False, "reason": "evolution_log_missing"}
102
+
103
+ repo_root = resolve_repo_root()
104
+ normalized_files: list[str] = []
105
+ seen: set[str] = set()
106
+ for filepath in files_changed:
107
+ rel = normalize_public_path(filepath, repo_root=repo_root)
108
+ if rel and rel not in seen:
109
+ normalized_files.append(rel)
110
+ seen.add(rel)
111
+ if not normalized_files:
112
+ return {"ok": False, "reason": "no_public_files"}
113
+
114
+ proposal = str(title or "").strip()[:300] or "Managed core autofix queued for public port"
115
+ clean_reasoning = str(reasoning or "").strip()[:4000] or "Queued for public-core port."
116
+ payload = dict(metadata or {})
117
+ payload.setdefault("source", source)
118
+ payload["files"] = normalized_files
119
+
120
+ existing = conn.execute(
121
+ """SELECT id, status
122
+ FROM evolution_log
123
+ WHERE classification = ?
124
+ AND proposal = ?
125
+ AND files_changed = ?
126
+ AND status IN (?, 'draft_pr_created', 'skipped_duplicate_existing_pr')
127
+ ORDER BY id DESC
128
+ LIMIT 1""",
129
+ (
130
+ QUEUE_CLASSIFICATION,
131
+ proposal,
132
+ json.dumps(normalized_files),
133
+ QUEUE_STATUS_PENDING,
134
+ ),
135
+ ).fetchone()
136
+ if existing:
137
+ return {
138
+ "ok": True,
139
+ "queued": False,
140
+ "log_id": int(existing["id"]),
141
+ "status": str(existing["status"] or ""),
142
+ "files_changed": normalized_files,
143
+ }
144
+
145
+ cur = conn.execute(
146
+ """INSERT INTO evolution_log (
147
+ cycle_number, dimension, proposal, classification, reasoning, status, files_changed, test_result
148
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
149
+ (
150
+ 0,
151
+ "public_core",
152
+ proposal,
153
+ QUEUE_CLASSIFICATION,
154
+ clean_reasoning,
155
+ QUEUE_STATUS_PENDING,
156
+ json.dumps(normalized_files),
157
+ json.dumps(payload, ensure_ascii=False),
158
+ ),
159
+ )
160
+ return {
161
+ "ok": True,
162
+ "queued": True,
163
+ "log_id": int(cur.lastrowid),
164
+ "status": QUEUE_STATUS_PENDING,
165
+ "files_changed": normalized_files,
166
+ }
167
+
168
+
169
+ def list_pending_public_port_candidates(
170
+ conn: sqlite3.Connection,
171
+ *,
172
+ limit: int = 3,
173
+ ) -> list[dict]:
174
+ rows = conn.execute(
175
+ """SELECT id, created_at, proposal, reasoning, status, files_changed, test_result
176
+ FROM evolution_log
177
+ WHERE classification = ?
178
+ AND status = ?
179
+ ORDER BY created_at ASC, id ASC
180
+ LIMIT ?""",
181
+ (QUEUE_CLASSIFICATION, QUEUE_STATUS_PENDING, max(1, int(limit))),
182
+ ).fetchall()
183
+ results: list[dict] = []
184
+ for row in rows:
185
+ metadata = {}
186
+ raw_payload = str(row["test_result"] or "").strip()
187
+ if raw_payload:
188
+ try:
189
+ parsed = json.loads(raw_payload)
190
+ if isinstance(parsed, dict):
191
+ metadata = parsed
192
+ except Exception:
193
+ metadata = {"raw": raw_payload}
194
+ files_changed = []
195
+ raw_files = str(row["files_changed"] or "").strip()
196
+ if raw_files:
197
+ try:
198
+ parsed_files = json.loads(raw_files)
199
+ if isinstance(parsed_files, list):
200
+ files_changed = [str(item).strip() for item in parsed_files if str(item).strip()]
201
+ except Exception:
202
+ pass
203
+ results.append(
204
+ {
205
+ "id": int(row["id"]),
206
+ "created_at": str(row["created_at"] or ""),
207
+ "title": str(row["proposal"] or ""),
208
+ "reasoning": str(row["reasoning"] or ""),
209
+ "status": str(row["status"] or ""),
210
+ "files_changed": files_changed,
211
+ "metadata": metadata,
212
+ }
213
+ )
214
+ return results
215
+
216
+
217
+ def update_public_port_candidate(
218
+ conn: sqlite3.Connection,
219
+ log_id: int,
220
+ *,
221
+ status: str,
222
+ metadata_patch: dict | None = None,
223
+ ) -> None:
224
+ row = conn.execute(
225
+ "SELECT test_result FROM evolution_log WHERE id = ? LIMIT 1",
226
+ (int(log_id),),
227
+ ).fetchone()
228
+ payload: dict = {}
229
+ if row and str(row["test_result"] or "").strip():
230
+ try:
231
+ parsed = json.loads(str(row["test_result"]))
232
+ if isinstance(parsed, dict):
233
+ payload = parsed
234
+ except Exception:
235
+ payload = {"raw": str(row["test_result"])}
236
+ if metadata_patch:
237
+ payload.update(metadata_patch)
238
+ conn.execute(
239
+ "UPDATE evolution_log SET status = ?, test_result = ? WHERE id = ?",
240
+ (status, json.dumps(payload, ensure_ascii=False), int(log_id)),
241
+ )
@@ -197,14 +197,14 @@ def main():
197
197
  log("Catch-Up already running; skipping overlapping invocation.")
198
198
  return
199
199
 
200
- _heal_personal_schedules()
201
- state = load_state()
202
- tasks = catchup_candidates()
203
-
204
200
  ran = 0
205
201
  skipped = 0
206
202
  skipped_out_of_window = 0
207
203
  try:
204
+ _heal_personal_schedules()
205
+ state = load_state()
206
+ tasks = catchup_candidates()
207
+
208
208
  for candidate in tasks:
209
209
  name = candidate["cron_id"]
210
210
  if not candidate.get("missed"):
@@ -221,19 +221,19 @@ def main():
221
221
  log(f" {name} — missed scheduled run due at {due_at}, catching up...")
222
222
  if run_task(candidate, state):
223
223
  ran += 1
224
- finally:
225
- lock_handle.close()
226
224
 
227
- if ran == 0 and skipped_out_of_window == 0:
228
- log("All tasks up to date, nothing to catch up.")
229
- elif ran >= 3:
230
- # Many tasks caught up — ask CLI to assess system state
231
- _cli_post_catchup_assessment(ran, skipped, state)
232
- else:
233
- suffix = f", {skipped_out_of_window} outside recovery window" if skipped_out_of_window else ""
234
- log(f"Caught up {ran} tasks, {skipped} already current{suffix}.")
225
+ if ran == 0 and skipped_out_of_window == 0:
226
+ log("All tasks up to date, nothing to catch up.")
227
+ elif ran >= 3:
228
+ # Many tasks caught up — ask CLI to assess system state
229
+ _cli_post_catchup_assessment(ran, skipped, state)
230
+ else:
231
+ suffix = f", {skipped_out_of_window} outside recovery window" if skipped_out_of_window else ""
232
+ log(f"Caught up {ran} tasks, {skipped} already current{suffix}.")
235
233
 
236
- log("=== Catch-Up complete ===")
234
+ log("=== Catch-Up complete ===")
235
+ finally:
236
+ lock_handle.close()
237
237
 
238
238
 
239
239
  def _cli_post_catchup_assessment(ran: int, skipped: int, state: dict):