nexo-brain 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -143,19 +143,26 @@ The installer handles everything:
143
143
  Generating operator instructions...
144
144
 
145
145
  ╔══════════════════════════════════════════════════════════╗
146
- ║ Atlas is ready.
147
- ║ Open Claude Code and start a conversation. ║
146
+ ║ Atlas is ready. Type 'atlas' to start.
148
147
  ╚══════════════════════════════════════════════════════════╝
149
148
  ```
150
149
 
151
- Open Claude Code and start working. Atlas will introduce itself on the first message.
150
+ ### Starting a Session
151
+
152
+ The installer creates a shell alias with your chosen name. Just type it:
153
+
154
+ ```bash
155
+ atlas
156
+ ```
157
+
158
+ That's it. No need to run `claude` manually. Atlas will greet you immediately — adapted to the time of day, resuming from where you left off if there's a previous session. No cold starts, no waiting for your input.
152
159
 
153
160
  ### What Gets Installed
154
161
 
155
162
  | Component | What | Where |
156
163
  |-----------|------|-------|
157
164
  | Cognitive engine | Python: fastembed, numpy, vector search | pip packages |
158
- | MCP server | 50+ tools for memory, learning, guard | ~/.nexo/ |
165
+ | MCP server | 76 tools for memory, learning, guard | ~/.nexo/ |
159
166
  | Plugins | Guard, episodic memory, cognitive memory, entities, preferences | ~/.nexo/plugins/ |
160
167
  | Hooks | Session capture, briefing, stop detection | ~/.nexo/hooks/ |
161
168
  | LaunchAgents | Decay, sleep, audit, postmortem, catch-up | ~/Library/LaunchAgents/ |
@@ -166,27 +173,31 @@ Open Claude Code and start working. Atlas will introduce itself on the first mes
166
173
 
167
174
  - **macOS** (Linux support planned)
168
175
  - **Node.js 18+** (for the installer)
176
+ - **Claude Opus (latest version) strongly recommended.** NEXO provides 76 MCP tools across 16 categories. This cognitive load requires a top-tier model with large context window. Smaller models (Haiku, Sonnet) may struggle with tool selection and produce inconsistent results. Opus handles all 76 tools without hesitation.
169
177
  - Python 3, Homebrew, and Claude Code are installed automatically if missing.
170
178
 
171
179
  ## Architecture
172
180
 
173
- ### 50+ MCP Tools
174
-
175
- | Category | Tools | Purpose |
176
- |----------|-------|---------|
177
- | Cognitive (8) | retrieve, stats, inspect, metrics, dissonance, resolve, sentiment, trust | The brain |
178
- | Guard (3) | check, stats, log_repetition | Error prevention |
179
- | Episodic (10) | changes, decisions, session diary, recall | What happened and why |
180
- | Sessions (3) | startup, heartbeat, status | Session lifecycle |
181
- | Reminders (4) | create, update, complete, delete | User's tasks |
182
- | Followups (4) | create, update, complete, delete | System's tasks |
183
- | Learnings (5) | add, search, update, delete, list | Error patterns |
184
- | Entities (5) | search, create, update, delete, list | People, services, URLs |
185
- | Preferences (4) | get, set, list, delete | Observed preferences |
186
- | Credentials (5) | create, get, update, delete, list | Secure storage |
187
- | Agents (5) | get, create, update, delete, list | Agent delegation |
188
- | Backup (3) | now, list, restore | Data safety |
189
- | Evolution (5) | propose, approve, reject, status, history | Self-improvement |
181
+ ### 76 MCP Tools across 16 Categories
182
+
183
+ | Category | Count | Tools | Purpose |
184
+ |----------|-------|-------|---------|
185
+ | Cognitive | 8 | retrieve, stats, inspect, metrics, dissonance, resolve, sentiment, trust | The brain — memory, RAG, trust, mood |
186
+ | Guard | 3 | check, stats, log_repetition | Metacognitive error prevention |
187
+ | Episodic | 10 | change_log/search/commit, decision_log/outcome/search, review_queue, diary_write/read, recall | What happened and why |
188
+ | Sessions | 3 | startup, heartbeat, status | Session lifecycle + context shift detection |
189
+ | Coordination | 7 | track, untrack, files, send, ask, answer, check_answer | Multi-session file coordination + messaging |
190
+ | Reminders | 5 | list, create, update, complete, delete | User's tasks and deadlines |
191
+ | Followups | 4 | create, update, complete, delete | System's autonomous verification tasks |
192
+ | Learnings | 5 | add, search, update, delete, list | Error patterns and prevention rules |
193
+ | Credentials | 5 | create, get, update, delete, list | Secure local credential storage |
194
+ | Task History | 3 | log, list, frequency | Execution tracking and overdue alerts |
195
+ | Menu | 1 | menu | Operations center with box-drawing UI |
196
+ | Entities | 5 | search, create, update, delete, list | People, services, URLs |
197
+ | Preferences | 4 | get, set, list, delete | Observed user preferences |
198
+ | Agents | 5 | get, create, update, delete, list | Agent delegation registry |
199
+ | Backup | 3 | now, list, restore | SQLite data safety |
200
+ | Evolution | 5 | propose, approve, reject, status, history | Self-improvement proposals |
190
201
 
191
202
  ### Plugin System
192
203
 
@@ -127,7 +127,74 @@ async function main() {
127
127
  log(`Got it. I'm ${operatorName}.`);
128
128
  console.log("");
129
129
 
130
- // Step 2: Permission to scan
130
+ // Step 2: Personality Calibration
131
+ log("Let's calibrate my personality to work best with you.");
132
+ log("(These can be changed anytime via nexo_preference_set)");
133
+ console.log("");
134
+
135
+ const autonomyAnswer = await ask(
136
+ " How autonomous should I be?\n" +
137
+ " 1. Ask before most actions (conservative)\n" +
138
+ " 2. Act on routine tasks, ask on important ones (balanced)\n" +
139
+ " 3. Act first, inform after — only ask when truly uncertain (full autonomy)\n" +
140
+ " > "
141
+ );
142
+ const autonomyLevel = ["conservative", "balanced", "full"][parseInt(autonomyAnswer.trim()) - 1] || "balanced";
143
+
144
+ const communicationAnswer = await ask(
145
+ "\n How should I communicate?\n" +
146
+ " 1. Concise — just results, no filler (expert user)\n" +
147
+ " 2. Balanced — brief explanations when useful\n" +
148
+ " 3. Detailed — explain reasoning and trade-offs\n" +
149
+ " > "
150
+ );
151
+ const communicationStyle = ["concise", "balanced", "detailed"][parseInt(communicationAnswer.trim()) - 1] || "balanced";
152
+
153
+ const honestyAnswer = await ask(
154
+ "\n When I disagree with your approach, should I:\n" +
155
+ " 1. Push back firmly and explain why\n" +
156
+ " 2. Mention it briefly but follow your lead\n" +
157
+ " 3. Just do what you ask\n" +
158
+ " > "
159
+ );
160
+ const honestyLevel = ["firm-pushback", "mention-and-follow", "just-execute"][parseInt(honestyAnswer.trim()) - 1] || "firm-pushback";
161
+
162
+ const proactivityAnswer = await ask(
163
+ "\n How proactive should I be?\n" +
164
+ " 1. Only do what's asked\n" +
165
+ " 2. Suggest improvements when I spot them\n" +
166
+ " 3. Fix things I notice without asking, and propose optimizations\n" +
167
+ " > "
168
+ );
169
+ const proactivityLevel = ["reactive", "suggestive", "proactive"][parseInt(proactivityAnswer.trim()) - 1] || "proactive";
170
+
171
+ const errorAnswer = await ask(
172
+ "\n When I make a mistake, how should I handle it?\n" +
173
+ " 1. Brief acknowledgment, fix it, move on\n" +
174
+ " 2. Explain what went wrong and what I learned\n" +
175
+ " > "
176
+ );
177
+ const errorHandling = ["brief-fix", "explain-and-learn"][parseInt(errorAnswer.trim()) - 1] || "brief-fix";
178
+
179
+ console.log("");
180
+ log(`Calibrated: autonomy=${autonomyLevel}, communication=${communicationStyle}, honesty=${honestyLevel}, proactivity=${proactivityLevel}, errors=${errorHandling}`);
181
+ console.log("");
182
+
183
+ // Save calibration
184
+ const calibration = {
185
+ autonomy: autonomyLevel,
186
+ communication: communicationStyle,
187
+ honesty: honestyLevel,
188
+ proactivity: proactivityLevel,
189
+ error_handling: errorHandling,
190
+ calibrated_at: new Date().toISOString(),
191
+ };
192
+ fs.writeFileSync(
193
+ path.join(NEXO_HOME, "brain", "calibration.json"),
194
+ JSON.stringify(calibration, null, 2)
195
+ );
196
+
197
+ // Step 3: Permission to scan
131
198
  const scanAnswer = await ask(
132
199
  " Can I explore your workspace to learn about your projects? (y/n) > "
133
200
  );
@@ -533,7 +600,33 @@ Operator name: ${operatorName}
533
600
  log("Caffeinate enabled — Mac will stay awake for cognitive processes.");
534
601
  }
535
602
 
536
- // Step 8: Generate CLAUDE.md template
603
+ // Step 8: Create shell alias so user can just type the operator's name
604
+ log("Creating shell alias...");
605
+ const aliasName = operatorName.toLowerCase();
606
+ const aliasLine = `alias ${aliasName}='claude --dangerously-skip-permissions "."'`;
607
+ const aliasComment = `# ${operatorName} — start Claude Code with ${operatorName} speaking first`;
608
+
609
+ // Detect shell and add alias
610
+ const userShell = process.env.SHELL || "/bin/bash";
611
+ const rcFile = userShell.includes("zsh")
612
+ ? path.join(require("os").homedir(), ".zshrc")
613
+ : path.join(require("os").homedir(), ".bash_profile");
614
+
615
+ let rcContent = "";
616
+ if (fs.existsSync(rcFile)) {
617
+ rcContent = fs.readFileSync(rcFile, "utf8");
618
+ }
619
+
620
+ if (!rcContent.includes(`alias ${aliasName}=`)) {
621
+ fs.appendFileSync(rcFile, `\n${aliasComment}\n${aliasLine}\n`);
622
+ log(`Added '${aliasName}' alias to ${path.basename(rcFile)}`);
623
+ log(`After setup, open a new terminal and type: ${aliasName}`);
624
+ } else {
625
+ log(`Alias '${aliasName}' already exists in ${path.basename(rcFile)}`);
626
+ }
627
+ console.log("");
628
+
629
+ // Step 9: Generate CLAUDE.md template
537
630
  log("Generating operator instructions...");
538
631
  const templateSrc = path.join(templateDir, "CLAUDE.md.template");
539
632
  let claudeMd = "";
@@ -568,16 +661,7 @@ See ~/.nexo/ for configuration.
568
661
  " ╔══════════════════════════════════════════════════════════╗"
569
662
  );
570
663
  console.log(
571
- ` ║ ${operatorName} is ready.${" ".repeat(Math.max(0, 39 - operatorName.length))}║`
572
- );
573
- console.log(
574
- " ║ ║"
575
- );
576
- console.log(
577
- " ║ Open Claude Code and start a conversation. ║"
578
- );
579
- console.log(
580
- ` ║ ${operatorName} will introduce ${operatorName.length > 4 ? "itself" : "itself"} on first message.${" ".repeat(Math.max(0, 27 - operatorName.length))}║`
664
+ ` ║ ${operatorName} is ready. Type '${aliasName}' to start.${" ".repeat(Math.max(0, 30 - operatorName.length - aliasName.length))}║`
581
665
  );
582
666
  console.log(
583
667
  " ╚══════════════════════════════════════════════════════════╝"
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
+ "mcpName": "io.github.wazionapps/nexo",
4
5
  "description": "NEXO — Cognitive co-operator for Claude Code. Atkinson-Shiffrin memory, semantic RAG, trust scoring, and metacognitive error prevention.",
5
6
  "bin": {
6
- "nexo-brain": "./bin/create-nexo.js"
7
+ "nexo-brain": "./bin/nexo-brain.js"
7
8
  },
8
9
  "keywords": [
9
10
  "claude-code",
@@ -8,7 +8,6 @@ runs them in the correct order.
8
8
 
9
9
  Scheduled tasks (ordered by intended run time):
10
10
  03:00 — cognitive-decay (Ebbinghaus decay + STM→LTM promotion)
11
- 03:00 — evolution (weekly, Sundays only)
12
11
  04:00 — sleep (session cleanup)
13
12
  07:00 — self-audit (health checks + weekly cognitive GC on Sundays)
14
13
  23:30 — postmortem (consolidation + sensory register)
@@ -24,15 +23,14 @@ import sys
24
23
  from datetime import datetime, timedelta
25
24
  from pathlib import Path
26
25
 
27
- HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
28
- LOG_DIR = HOME / "claude" / "logs"
26
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
27
+ LOG_DIR = NEXO_HOME / "logs"
29
28
  LOG_DIR.mkdir(parents=True, exist_ok=True)
30
29
  LOG_FILE = LOG_DIR / "catchup.log"
31
- STATE_FILE = HOME / "claude" / "operations" / ".catchup-state.json"
30
+ STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
31
+ SCRIPTS = NEXO_HOME / "src" / "scripts"
32
32
 
33
- PYTHON_BREW = "/opt/homebrew/bin/python3"
34
- PYTHON_SYS = "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3"
35
- SCRIPTS = HOME / "claude" / "scripts"
33
+ PYTHON = sys.executable
36
34
 
37
35
 
38
36
  def log(msg: str):
@@ -95,7 +93,7 @@ def should_run(task_name: str, hour: int, minute: int, state: dict, weekday: int
95
93
  return last_run < last_scheduled
96
94
 
97
95
 
98
- def run_task(name: str, python: str, script: str, state: dict) -> bool:
96
+ def run_task(name: str, script: str, state: dict) -> bool:
99
97
  """Execute a task and update state."""
100
98
  script_path = str(SCRIPTS / script)
101
99
  if not Path(script_path).exists():
@@ -105,9 +103,9 @@ def run_task(name: str, python: str, script: str, state: dict) -> bool:
105
103
  log(f" RUNNING {name}: {script}")
106
104
  try:
107
105
  result = subprocess.run(
108
- [python, script_path],
106
+ [PYTHON, script_path],
109
107
  capture_output=True, text=True, timeout=300,
110
- env={**os.environ, "HOME": str(HOME), "NEXO_CATCHUP": "1"}
108
+ env={**os.environ, "HOME": str(Path.home()), "NEXO_HOME": str(NEXO_HOME), "NEXO_CATCHUP": "1"}
111
109
  )
112
110
  if result.returncode == 0:
113
111
  log(f" OK {name} (exit 0)")
@@ -128,41 +126,23 @@ def run_task(name: str, python: str, script: str, state: dict) -> bool:
128
126
 
129
127
  def main():
130
128
  log("=== NEXO Catch-Up starting (boot/wake) ===")
131
-
132
- # Auto-update check FIRST (before running any other tasks)
133
- update_script = SCRIPTS / "nexo-auto-update.py"
134
- if update_script.exists():
135
- log("Checking for updates...")
136
- try:
137
- python_for_update = PYTHON_BREW if os.path.exists(PYTHON_BREW) else PYTHON_SYS
138
- subprocess.run(
139
- [python_for_update, str(update_script)],
140
- capture_output=True, text=True, timeout=60,
141
- env={**os.environ, "NEXO_HOME": str(HOME)}
142
- )
143
- except Exception as e:
144
- log(f" Update check failed: {e}")
145
-
146
129
  state = load_state()
147
130
 
148
131
  # Define tasks in execution order (matching their intended schedule order)
149
- # Find Python — prefer homebrew, fallback to system
150
- python_path = PYTHON_BREW if os.path.exists(PYTHON_BREW) else PYTHON_SYS
151
-
152
132
  tasks = [
153
- # (name, hour, minute, python, script, weekday)
154
- ("cognitive-decay", 3, 0, python_path, "nexo-cognitive-decay.py", None),
155
- ("sleep", 4, 0, python_path, "nexo-sleep.py", None),
156
- ("self-audit", 7, 0, python_path, "nexo-daily-self-audit.py", None),
157
- ("postmortem", 23, 30, python_path, "nexo-postmortem-consolidator.py", None),
133
+ # (name, hour, minute, script, weekday)
134
+ ("cognitive-decay", 3, 0, "nexo-cognitive-decay.py", None),
135
+ ("sleep", 4, 0, "nexo-sleep.py", None),
136
+ ("self-audit", 7, 0, "nexo-daily-self-audit.py", None),
137
+ ("postmortem", 23, 30, "nexo-postmortem-consolidator.py", None),
158
138
  ]
159
139
 
160
140
  ran = 0
161
141
  skipped = 0
162
- for name, hour, minute, python, script, weekday in tasks:
142
+ for name, hour, minute, script, weekday in tasks:
163
143
  if should_run(name, hour, minute, state, weekday):
164
144
  log(f" {name} — missed scheduled run, catching up...")
165
- if run_task(name, python, script, state):
145
+ if run_task(name, script, state):
166
146
  ran += 1
167
147
  else:
168
148
  skipped += 1
@@ -5,11 +5,13 @@ import json
5
5
  import sys
6
6
  from pathlib import Path
7
7
  from datetime import datetime
8
+ import os
8
9
 
9
- sys.path.insert(0, str(Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))))
10
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
11
+ sys.path.insert(0, str(NEXO_HOME / "src"))
10
12
  import cognitive
11
13
 
12
- STATE_FILE = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "operations" / ".catchup-state.json"
14
+ STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
13
15
 
14
16
 
15
17
  def update_catchup_state():
@@ -2,7 +2,7 @@
2
2
  """
3
3
  NEXO Daily Self-Audit
4
4
  Proactively scans for common issues before they become problems.
5
- Runs via launchd at 7:00 AM daily. Results saved to ~/claude/logs/self-audit.log
5
+ Runs via launchd at 7:00 AM daily. Results saved to NEXO_HOME/logs/self-audit.log
6
6
  """
7
7
  import json
8
8
  import os
@@ -14,17 +14,17 @@ import hashlib
14
14
  from datetime import datetime, timedelta
15
15
  from pathlib import Path
16
16
 
17
- LOG_DIR = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "logs"
17
+ NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
18
+
19
+ LOG_DIR = NEXO_HOME / "logs"
18
20
  LOG_DIR.mkdir(parents=True, exist_ok=True)
19
21
  LOG_FILE = LOG_DIR / "self-audit.log"
20
- NEXO_DB = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "nexo.db"
21
- # Project directory for git checks (user configurable)
22
- HASH_REGISTRY = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "scripts" / ".watchdog-hashes"
23
- SNAPSHOT_GOLDEN = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "snapshots" / "golden" / "files" / "claude"
24
- RUNTIME_PREFLIGHT_SUMMARY = LOG_DIR / "runtime-preflight-summary.json"
25
- WATCHDOG_SMOKE_SUMMARY = LOG_DIR / "watchdog-smoke-summary.json"
26
- RESTORE_LOG = LOG_DIR / "snapshot-restores.log"
27
- CORTEX_LOG_DIR = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "cortex" / "logs"
22
+ NEXO_DB = NEXO_HOME / "nexo.db"
23
+ # Optional: project directory for git checks set via env var
24
+ PROJECT_DIR_STR = os.environ.get("NEXO_PROJECT_DIR", "")
25
+ PROJECT_DIR = Path(PROJECT_DIR_STR) if PROJECT_DIR_STR else None
26
+ HASH_REGISTRY = NEXO_HOME / "scripts" / ".watchdog-hashes"
27
+ CORTEX_LOG_DIR = NEXO_HOME / "cortex" / "logs"
28
28
 
29
29
  findings = []
30
30
 
@@ -72,12 +72,13 @@ def check_overdue_followups():
72
72
  finding("WARN", "followups", f"{len(rows)} overdue: {', '.join(r[0][:40] for r in rows[:5])}")
73
73
 
74
74
 
75
+ # ── Check 3: Git uncommitted changes in project dir ─────────────────────────
75
76
  def check_uncommitted_changes():
76
- if not WAZION_DIR.exists():
77
+ if not PROJECT_DIR or not PROJECT_DIR.exists():
77
78
  return
78
79
  result = subprocess.run(
79
80
  ["git", "status", "--porcelain"],
80
- cwd=str(WAZION_DIR), capture_output=True, text=True
81
+ cwd=str(PROJECT_DIR), capture_output=True, text=True
81
82
  )
82
83
  lines = [l for l in result.stdout.strip().split("\n") if l.strip()]
83
84
  if len(lines) > 10:
@@ -101,7 +102,7 @@ def check_cron_errors():
101
102
 
102
103
  # ── Check 5: Evolution failures ─────────────────────────────────────────
103
104
  def check_evolution_health():
104
- obj_file = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "cortex" / "evolution-objective.json"
105
+ obj_file = NEXO_HOME / "cortex" / "evolution-objective.json"
105
106
  if not obj_file.exists():
106
107
  return
107
108
  obj = json.loads(obj_file.read_text())
@@ -224,122 +225,10 @@ def check_memory_reviews():
224
225
  finding("INFO", "memory", f"{total_due} memory reviews due ({due_decisions} decisions, {due_learnings} learnings)")
225
226
 
226
227
 
227
- def _sha256(path: Path) -> str:
228
- return hashlib.sha256(path.read_bytes()).hexdigest()
229
-
230
-
231
- # ── Check 12: Watchdog registry sanity ──────────────────────────────────
232
- def check_watchdog_registry():
233
- if not HASH_REGISTRY.exists():
234
- finding("WARN", "watchdog", "hash registry missing")
235
- return
236
- text = HASH_REGISTRY.read_text(errors="ignore")
237
- forbidden = ["CLAUDE.md", "db.py", "server.py", "plugin_loader.py", "cortex-wrapper.py"]
238
- bad = [name for name in forbidden if name in text]
239
- if bad:
240
- finding("ERROR", "watchdog", f"mutable files still protected by watchdog: {', '.join(bad)}")
241
-
242
-
243
- # ── Check 13: Snapshot drift on protected recovery files ────────────────
244
- def check_snapshot_sync():
245
- pairs = [
246
- (Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "db.py", SNAPSHOT_GOLDEN / "nexo-mcp" / "db.py"),
247
- (Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "cortex" / "cortex-wrapper.py", SNAPSHOT_GOLDEN / "cortex" / "cortex-wrapper.py"),
248
- (Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "cortex" / "evolution_cycle.py", SNAPSHOT_GOLDEN / "cortex" / "evolution_cycle.py"),
249
- ]
250
- drift = []
251
- for live, snap in pairs:
252
- if not live.exists() or not snap.exists():
253
- drift.append(live.name)
254
- continue
255
- if _sha256(live) != _sha256(snap):
256
- drift.append(live.name)
257
- if drift:
258
- finding("WARN", "snapshots", f"golden snapshot drift: {', '.join(drift)}")
259
-
260
-
261
- # ── Check 14: Recent restore activity ───────────────────────────────────
262
- def check_restore_activity():
263
- if not RESTORE_LOG.exists():
264
- return
265
- cutoff_day = datetime.now() - timedelta(days=1)
266
- current_hour_prefix = datetime.now().strftime("%Y-%m-%d %H")
267
- recent_day = 0
268
- recent_hour = 0
269
- for line in RESTORE_LOG.read_text(errors="ignore").splitlines():
270
- if not line.startswith("["):
271
- continue
272
- if "/.codex/memories/nexo-" in line:
273
- continue
274
- try:
275
- ts = datetime.strptime(line[1:20], "%Y-%m-%d %H:%M:%S")
276
- except ValueError:
277
- continue
278
- if ts >= cutoff_day:
279
- recent_day += 1
280
- if line[1:14] == current_hour_prefix:
281
- recent_hour += 1
282
- if recent_hour > 2:
283
- finding("ERROR", "restore", f"{recent_hour} snapshot restores in last hour")
284
- elif recent_day > 5:
285
- finding("WARN", "restore", f"{recent_day} snapshot restores in last 24h")
286
- elif recent_day > 0:
287
- finding("INFO", "restore", f"{recent_day} snapshot restores in last 24h (historical activity)")
288
-
289
-
290
- # ── Check 15: Bad model responses ───────────────────────────────────────
291
- def check_bad_responses():
292
- if not CORTEX_LOG_DIR.exists():
293
- return
294
- cutoff = datetime.now() - timedelta(days=1)
295
- bad = [
296
- p for p in CORTEX_LOG_DIR.glob("bad-response-*.json")
297
- if datetime.fromtimestamp(p.stat().st_mtime) >= cutoff
298
- ]
299
- if bad:
300
- finding("WARN", "cortex", f"{len(bad)} bad model responses in last 24h")
301
-
302
-
303
- # ── Check 16: Runtime preflight freshness ───────────────────────────────
304
- def check_runtime_preflight():
305
- if not RUNTIME_PREFLIGHT_SUMMARY.exists():
306
- finding("WARN", "preflight", "runtime preflight summary missing")
307
- return
308
- data = json.loads(RUNTIME_PREFLIGHT_SUMMARY.read_text())
309
- ts = data.get("timestamp")
310
- try:
311
- when = datetime.fromisoformat(ts)
312
- except Exception:
313
- finding("WARN", "preflight", "runtime preflight timestamp invalid")
314
- return
315
- if when < datetime.now() - timedelta(days=1):
316
- finding("WARN", "preflight", "runtime preflight older than 24h")
317
- if not data.get("ok", False):
318
- finding("ERROR", "preflight", "runtime preflight failing")
319
-
320
-
321
- # ── Check 17: Watchdog smoke freshness ──────────────────────────────────
322
- def check_watchdog_smoke():
323
- if not WATCHDOG_SMOKE_SUMMARY.exists():
324
- finding("WARN", "watchdog", "watchdog smoke summary missing")
325
- return
326
- data = json.loads(WATCHDOG_SMOKE_SUMMARY.read_text())
327
- ts = data.get("timestamp")
328
- try:
329
- when = datetime.fromisoformat(ts)
330
- except Exception:
331
- finding("WARN", "watchdog", "watchdog smoke timestamp invalid")
332
- return
333
- if when < datetime.now() - timedelta(days=1):
334
- finding("WARN", "watchdog", "watchdog smoke older than 24h")
335
- if not data.get("ok", False):
336
- finding("ERROR", "watchdog", "watchdog smoke failing")
337
-
338
-
339
- # ── Check 18: Cognitive memory health ────────────────────────────────
228
+ # ── Check 12: Cognitive memory health ────────────────────────────────
340
229
  def check_cognitive_health():
341
230
  """Check cognitive.db health and run weekly GC on Sundays."""
342
- cognitive_db = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "cognitive.db"
231
+ cognitive_db = NEXO_HOME / "cognitive.db"
343
232
  if not cognitive_db.exists():
344
233
  finding("WARN", "cognitive", "cognitive.db not found")
345
234
  return
@@ -360,7 +249,6 @@ def check_cognitive_health():
360
249
 
361
250
  # Metrics report (spec section 9)
362
251
  try:
363
- sys.path.insert(0, str(Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))))
364
252
  import cognitive as cog
365
253
 
366
254
  metrics = cog.get_metrics(days=7)
@@ -373,7 +261,7 @@ def check_cognitive_health():
373
261
 
374
262
  if metrics["needs_multilingual"]:
375
263
  finding("WARN", "cognitive-metrics",
376
- f"Retrieval relevance {metrics['retrieval_relevance_pct']}% < 70% — consider switching to multilingual model (spec 13.3)")
264
+ f"Retrieval relevance {metrics['retrieval_relevance_pct']}% < 70% — consider switching to multilingual model")
377
265
 
378
266
  if metrics["retrieval_relevance_pct"] < 50 and metrics["total_retrievals"] >= 5:
379
267
  finding("ERROR", "cognitive-metrics",
@@ -399,21 +287,20 @@ def check_cognitive_health():
399
287
  except Exception as e:
400
288
  finding("WARN", "cognitive-metrics", f"Metrics collection failed: {e}")
401
289
 
402
- # Phase triggers monitoring (spec section 10)
290
+ # Phase triggers monitoring
403
291
  try:
404
- sys.path.insert(0, str(Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))))
405
292
  import cognitive as cog
406
293
 
407
294
  db_cog = cog._get_db()
408
295
 
409
296
  # v2.0: Procedural memory — trigger: >50 procedural change_logs
410
- procedural_markers = ['1.', '2.', '3.', 'step ', 'Step ', 'then ', 'first ', 'First ', '→', '->', 'SSH', 'scp', 'git commit', 'deploy']
297
+ procedural_markers = ['1.', '2.', '3.', 'step ', 'Step ', 'then ', 'first ', 'First ', '→', '->', 'git commit', 'deploy']
411
298
  changes = db_cog.execute('SELECT content FROM ltm_memories WHERE source_type = "change"').fetchall()
412
299
  procedural_count = sum(1 for r in changes if sum(1 for m in procedural_markers if m in r[0]) >= 2)
413
300
  if procedural_count >= 50:
414
- finding("WARN", "cognitive-phase", f"v2.0 TRIGGER MET: {procedural_count} procedural memories (>50). Implement Store 4 (memoria procedimental).")
301
+ finding("WARN", "cognitive-phase", f"v2.0 TRIGGER MET: {procedural_count} procedural memories (>50). Implement Store 4 (procedural memory).")
415
302
 
416
- # v2.1: MEMORY.md reduction — trigger: RAG relevance >80% for 30 days
303
+ # v2.1: MEMORY reduction — trigger: RAG relevance >80% for 30 days
417
304
  metrics_file = LOG_DIR / "cognitive-metrics-history.json"
418
305
  try:
419
306
  history = json.loads(metrics_file.read_text()) if metrics_file.exists() else []
@@ -437,7 +324,7 @@ def check_cognitive_health():
437
324
  last_30 = history[-30:]
438
325
  all_above_80 = all(h["relevance"] >= 80.0 for h in last_30)
439
326
  if all_above_80:
440
- finding("WARN", "cognitive-phase", "v2.1 TRIGGER MET: RAG relevance >80% for 30 consecutive days. Reduce MEMORY.md to ~20 lines.")
327
+ finding("WARN", "cognitive-phase", "v2.1 TRIGGER MET: RAG relevance >80% for 30 consecutive days. Consider reducing static memory files.")
441
328
 
442
329
  # v2.2: Dashboard — trigger: 30 days of metrics
443
330
  if len(history) >= 30:
@@ -448,8 +335,6 @@ def check_cognitive_health():
448
335
  if ltm_count >= 1000:
449
336
  finding("WARN", "cognitive-phase", f"v3.0 TRIGGER MET: {ltm_count} LTM vectors (>1000). Implement K-means clustering.")
450
337
 
451
- # v1.4: Multilingual — already checked in metrics section above
452
-
453
338
  except Exception as e:
454
339
  finding("WARN", "cognitive-phase", f"Phase trigger check failed: {e}")
455
340
 
@@ -457,7 +342,6 @@ def check_cognitive_health():
457
342
  if datetime.now().weekday() == 6:
458
343
  log(" Running weekly cognitive GC (Sunday)...")
459
344
  try:
460
- sys.path.insert(0, str(Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))))
461
345
  import cognitive as cog
462
346
 
463
347
  # 1. Delete STM with strength < 0.1 and > 30 days
@@ -478,6 +362,11 @@ def check_cognitive_health():
478
362
 
479
363
  # ── Main ────────────────────────────────────────────────────────────────
480
364
  def main():
365
+ # Ensure cognitive module is importable
366
+ src_dir = NEXO_HOME / "src"
367
+ if src_dir.exists() and str(src_dir) not in sys.path:
368
+ sys.path.insert(0, str(src_dir))
369
+
481
370
  log("=" * 60)
482
371
  log("NEXO Daily Self-Audit starting")
483
372
 
@@ -492,12 +381,6 @@ def main():
492
381
  check_repetition_rate()
493
382
  check_unused_learnings()
494
383
  check_memory_reviews()
495
- check_watchdog_registry()
496
- check_snapshot_sync()
497
- check_restore_activity()
498
- check_bad_responses()
499
- check_runtime_preflight()
500
- check_watchdog_smoke()
501
384
  check_cognitive_health()
502
385
 
503
386
  errors = sum(1 for f in findings if f["severity"] == "ERROR")
@@ -517,7 +400,7 @@ def main():
517
400
  # Register successful run for catch-up
518
401
  try:
519
402
  import json as _json
520
- _state_file = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))) / "operations" / ".catchup-state.json"
403
+ _state_file = NEXO_HOME / "operations" / ".catchup-state.json"
521
404
  _state = _json.loads(_state_file.read_text()) if _state_file.exists() else {}
522
405
  _state["self-audit"] = datetime.now().isoformat()
523
406
  _state_file.write_text(_json.dumps(_state, indent=2))