nexo-brain 1.2.2 → 1.2.3
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/LICENSE +21 -16
- package/README.md +2 -2
- package/package.json +4 -4
- package/src/__pycache__/db.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
- package/src/plugins/guard.py +36 -20
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
- package/src/rules/core-rules 2.json +329 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/scripts/nexo-watchdog.sh +645 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""NEXO Brain Rules Migration System.
|
|
3
|
+
|
|
4
|
+
Manages versioned core rules that ship with every installation.
|
|
5
|
+
Handles adding new rules, removing deprecated ones, and updating
|
|
6
|
+
the user's CLAUDE.md without touching their customizations.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from rules.migrate import migrate_rules
|
|
10
|
+
result = migrate_rules(nexo_home) # Returns dict with changes applied
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
RULES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "core-rules.json")
|
|
21
|
+
VERSION_KEY = "rules_version"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_core_rules() -> dict:
|
|
25
|
+
"""Load the current core rules definition."""
|
|
26
|
+
with open(RULES_FILE, "r") as f:
|
|
27
|
+
return json.load(f)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_installed_version(nexo_home: str) -> Optional[str]:
|
|
31
|
+
"""Get the rules version currently installed in the user's NEXO home."""
|
|
32
|
+
version_file = os.path.join(nexo_home, "brain", "rules_version.json")
|
|
33
|
+
if not os.path.exists(version_file):
|
|
34
|
+
return None
|
|
35
|
+
try:
|
|
36
|
+
with open(version_file, "r") as f:
|
|
37
|
+
data = json.load(f)
|
|
38
|
+
return data.get("version")
|
|
39
|
+
except (json.JSONDecodeError, KeyError):
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def save_installed_version(nexo_home: str, version: str, rule_ids: list[str]):
|
|
44
|
+
"""Record which rules version and rule IDs are installed."""
|
|
45
|
+
version_file = os.path.join(nexo_home, "brain", "rules_version.json")
|
|
46
|
+
os.makedirs(os.path.dirname(version_file), exist_ok=True)
|
|
47
|
+
data = {
|
|
48
|
+
"version": version,
|
|
49
|
+
"installed_rule_ids": rule_ids,
|
|
50
|
+
"installed_at": _now_iso(),
|
|
51
|
+
}
|
|
52
|
+
with open(version_file, "w") as f:
|
|
53
|
+
json.dump(data, f, indent=2)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_installed_rule_ids(nexo_home: str) -> list[str]:
|
|
57
|
+
"""Get the list of rule IDs currently installed."""
|
|
58
|
+
version_file = os.path.join(nexo_home, "brain", "rules_version.json")
|
|
59
|
+
if not os.path.exists(version_file):
|
|
60
|
+
return []
|
|
61
|
+
try:
|
|
62
|
+
with open(version_file, "r") as f:
|
|
63
|
+
data = json.load(f)
|
|
64
|
+
return data.get("installed_rule_ids", [])
|
|
65
|
+
except (json.JSONDecodeError, KeyError):
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def generate_rules_markdown(rules_data: dict) -> str:
|
|
70
|
+
"""Generate the Operational Codex markdown from core-rules.json."""
|
|
71
|
+
lines = [
|
|
72
|
+
"## Operational Codex (NON-NEGOTIABLE)",
|
|
73
|
+
"",
|
|
74
|
+
"These rules are the behavioral foundation of every cognitive co-operator.",
|
|
75
|
+
"They are derived from real production failures and validated through multi-AI debate.",
|
|
76
|
+
f"Rules version: {rules_data['_meta']['version']}",
|
|
77
|
+
"",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
for cat_key, cat in rules_data["categories"].items():
|
|
81
|
+
lines.append(f"### {cat['label']}")
|
|
82
|
+
lines.append("")
|
|
83
|
+
for rule in cat["rules"]:
|
|
84
|
+
tag = "BLOCKING" if rule["type"] == "blocking" else "ADVISORY"
|
|
85
|
+
lines.append(f"**{rule['id']}. {rule['rule']}** [{tag}]")
|
|
86
|
+
lines.append(f"_{rule['why']}_")
|
|
87
|
+
lines.append("")
|
|
88
|
+
|
|
89
|
+
return "\n".join(lines)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def find_codex_section(claude_md: str) -> tuple[int, int]:
|
|
93
|
+
"""Find the start and end positions of the Operational Codex section in CLAUDE.md."""
|
|
94
|
+
# Look for the section header
|
|
95
|
+
start_pattern = r"## Operational Codex \(NON-NEGOTIABLE\)"
|
|
96
|
+
start_match = re.search(start_pattern, claude_md)
|
|
97
|
+
if not start_match:
|
|
98
|
+
return (-1, -1)
|
|
99
|
+
|
|
100
|
+
start = start_match.start()
|
|
101
|
+
|
|
102
|
+
# Find the next ## section header after the codex
|
|
103
|
+
rest = claude_md[start_match.end():]
|
|
104
|
+
next_section = re.search(r"\n## [A-Z]", rest)
|
|
105
|
+
if next_section:
|
|
106
|
+
end = start_match.end() + next_section.start()
|
|
107
|
+
else:
|
|
108
|
+
end = len(claude_md)
|
|
109
|
+
|
|
110
|
+
return (start, end)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def migrate_rules(nexo_home: str, dry_run: bool = False) -> dict:
|
|
114
|
+
"""Migrate rules to the latest version.
|
|
115
|
+
|
|
116
|
+
Compares installed rules version with current core-rules.json.
|
|
117
|
+
Adds new rules, removes deprecated ones, updates CLAUDE.md.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
nexo_home: Path to NEXO home directory
|
|
121
|
+
dry_run: If True, show what would change without applying
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dict with: version_from, version_to, added, removed, unchanged, dry_run
|
|
125
|
+
"""
|
|
126
|
+
rules_data = load_core_rules()
|
|
127
|
+
current_version = rules_data["_meta"]["version"]
|
|
128
|
+
installed_version = get_installed_version(nexo_home)
|
|
129
|
+
installed_ids = set(get_installed_rule_ids(nexo_home))
|
|
130
|
+
|
|
131
|
+
# Collect all rule IDs from current version
|
|
132
|
+
current_ids = set()
|
|
133
|
+
for cat in rules_data["categories"].values():
|
|
134
|
+
for rule in cat["rules"]:
|
|
135
|
+
current_ids.add(rule["id"])
|
|
136
|
+
|
|
137
|
+
# Calculate diff
|
|
138
|
+
added = current_ids - installed_ids if installed_ids else current_ids
|
|
139
|
+
removed = installed_ids - current_ids if installed_ids else set()
|
|
140
|
+
unchanged = current_ids & installed_ids if installed_ids else set()
|
|
141
|
+
|
|
142
|
+
result = {
|
|
143
|
+
"version_from": installed_version or "none",
|
|
144
|
+
"version_to": current_version,
|
|
145
|
+
"added": sorted(added),
|
|
146
|
+
"removed": sorted(removed),
|
|
147
|
+
"unchanged": sorted(unchanged),
|
|
148
|
+
"total_rules": len(current_ids),
|
|
149
|
+
"dry_run": dry_run,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if installed_version == current_version and not added and not removed:
|
|
153
|
+
result["status"] = "up_to_date"
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
if dry_run:
|
|
157
|
+
result["status"] = "changes_pending"
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
# Apply: update the Operational Codex section in CLAUDE.md
|
|
161
|
+
claude_md_path = os.path.join(nexo_home, "CLAUDE.md")
|
|
162
|
+
if os.path.exists(claude_md_path):
|
|
163
|
+
with open(claude_md_path, "r") as f:
|
|
164
|
+
claude_md = f.read()
|
|
165
|
+
|
|
166
|
+
new_codex = generate_rules_markdown(rules_data)
|
|
167
|
+
start, end = find_codex_section(claude_md)
|
|
168
|
+
|
|
169
|
+
if start >= 0:
|
|
170
|
+
# Replace existing codex section
|
|
171
|
+
claude_md = claude_md[:start] + new_codex + "\n" + claude_md[end:]
|
|
172
|
+
else:
|
|
173
|
+
# Append codex after the first section
|
|
174
|
+
# Find the end of the first ## section
|
|
175
|
+
first_section_end = re.search(r"\n## ", claude_md[10:])
|
|
176
|
+
if first_section_end:
|
|
177
|
+
insert_pos = 10 + first_section_end.start()
|
|
178
|
+
claude_md = claude_md[:insert_pos] + "\n\n" + new_codex + "\n" + claude_md[insert_pos:]
|
|
179
|
+
else:
|
|
180
|
+
claude_md += "\n\n" + new_codex
|
|
181
|
+
|
|
182
|
+
with open(claude_md_path, "w") as f:
|
|
183
|
+
f.write(claude_md)
|
|
184
|
+
|
|
185
|
+
# Save version record
|
|
186
|
+
save_installed_version(nexo_home, current_version, sorted(current_ids))
|
|
187
|
+
|
|
188
|
+
result["status"] = "migrated"
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _now_iso() -> str:
|
|
193
|
+
from datetime import datetime
|
|
194
|
+
return datetime.utcnow().isoformat() + "Z"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
import sys
|
|
199
|
+
if len(sys.argv) < 2:
|
|
200
|
+
print("Usage: python migrate.py <nexo_home> [--dry-run]")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
home = sys.argv[1]
|
|
204
|
+
dry = "--dry-run" in sys.argv
|
|
205
|
+
|
|
206
|
+
result = migrate_rules(home, dry_run=dry)
|
|
207
|
+
print(json.dumps(result, indent=2))
|