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 +32 -21
- package/bin/{create-nexo.js → nexo-brain.js} +96 -12
- package/package.json +3 -2
- package/src/scripts/nexo-catchup.py +15 -35
- package/src/scripts/nexo-cognitive-decay.py +4 -2
- package/src/scripts/nexo-daily-self-audit.py +28 -145
- package/src/scripts/nexo-postmortem-consolidator.py +52 -56
- package/src/scripts/nexo-sleep.py +46 -54
- package/src/scripts/nexo-synthesis.py +81 -81
- package/templates/CLAUDE.md.template +114 -3
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
|
-
|
|
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 |
|
|
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
|
-
###
|
|
174
|
-
|
|
175
|
-
| Category | Tools | Purpose |
|
|
176
|
-
|
|
177
|
-
| Cognitive
|
|
178
|
-
| Guard
|
|
179
|
-
| Episodic
|
|
180
|
-
| Sessions
|
|
181
|
-
|
|
|
182
|
-
|
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
|
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
|
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:
|
|
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:
|
|
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,
|
|
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.
|
|
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/
|
|
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
|
-
|
|
28
|
-
LOG_DIR =
|
|
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 =
|
|
30
|
+
STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
|
|
31
|
+
SCRIPTS = NEXO_HOME / "src" / "scripts"
|
|
32
32
|
|
|
33
|
-
|
|
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,
|
|
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
|
-
[
|
|
106
|
+
[PYTHON, script_path],
|
|
109
107
|
capture_output=True, text=True, timeout=300,
|
|
110
|
-
env={**os.environ, "HOME": str(
|
|
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,
|
|
154
|
-
("cognitive-decay", 3, 0,
|
|
155
|
-
("sleep", 4, 0,
|
|
156
|
-
("self-audit", 7, 0,
|
|
157
|
-
("postmortem", 23, 30,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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 ', '→', '->', '
|
|
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 (
|
|
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
|
|
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.
|
|
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 =
|
|
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))
|