nexo-brain 2.1.0 → 2.3.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.
- package/README.md +7 -7
- package/bin/nexo-brain.js +53 -26
- package/package.json +1 -1
- package/scripts/migrate-to-unified 2.sh +813 -0
- package/scripts/migrate-v1.5-to-v1.6 2.py +778 -0
- package/scripts/migrate-v1.7-to-v1.8 2.py +214 -0
- package/scripts/migrate-v1.7-to-v1.8.py +2 -2
- package/scripts/nexo-preflight.sh +236 -0
- package/scripts/pre-commit-check 2.sh +55 -0
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +159 -0
- package/src/auto_update 2.py +634 -0
- package/src/auto_update.py +25 -0
- package/src/claim_graph 2.py +323 -0
- package/src/cognitive/__init__ 2.py +62 -0
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core 2.py +567 -0
- package/src/cognitive/_decay 2.py +382 -0
- package/src/cognitive/_ingest 2.py +892 -0
- package/src/cognitive/_memory 2.py +912 -0
- package/src/cognitive/_search 2.py +949 -0
- package/src/cognitive/_trust 2.py +464 -0
- package/src/cognitive/_trust.py +10 -36
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +106 -0
- package/src/crons/manifest.json +6 -13
- package/src/crons/sync 2.py +217 -0
- package/src/crons/sync.py +151 -6
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +789 -0
- package/src/db/__init__ 2.py +89 -0
- package/src/db/__init__.py +13 -0
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +417 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_cron_runs.py +74 -0
- package/src/db/_entities 2.py +178 -0
- package/src/db/_episodic 2.py +738 -0
- package/src/db/_episodic.py +40 -6
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_learnings 2.py +168 -0
- package/src/db/_reminders 2.py +338 -0
- package/src/db/_schema 2.py +364 -0
- package/src/db/_schema.py +64 -0
- package/src/db/_sessions 2.py +300 -0
- package/src/db/_skills.py +514 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/evolution_cycle 2.py +266 -0
- package/src/hnsw_index 2.py +254 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-session.sh +2 -0
- package/src/hooks/capture-tool-logs 2.sh +127 -0
- package/src/hooks/capture-tool-logs.sh +3 -2
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/inbox-hook.sh +3 -2
- package/src/hooks/post-compact 2.sh +148 -0
- package/src/hooks/post-compact.sh +1 -1
- package/src/hooks/pre-compact 2.sh +151 -0
- package/src/hooks/pre-compact.sh +1 -1
- package/src/hooks/session-start 2.sh +268 -0
- package/src/hooks/session-start.sh +6 -3
- package/src/hooks/session-stop 2.sh +140 -0
- package/src/hooks/session-stop.sh +14 -102
- package/src/kg_populate 2.py +290 -0
- package/src/knowledge_graph 2.py +257 -0
- package/src/maintenance 2.py +59 -0
- package/src/migrate_embeddings 2.py +122 -0
- package/src/plugin_loader 2.py +202 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +805 -0
- package/src/plugins/agents 2.py +52 -0
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +104 -0
- package/src/plugins/cognitive_memory 2.py +564 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +299 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +533 -0
- package/src/plugins/episodic_memory.py +5 -3
- package/src/plugins/evolution 2.py +115 -0
- package/src/plugins/guard 2.py +746 -0
- package/src/plugins/knowledge_graph_tools 2.py +105 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/schedule.py +212 -0
- package/src/plugins/skills.py +264 -0
- package/src/plugins/update 2.py +256 -0
- package/src/requirements 2.txt +12 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context 2.py +264 -0
- package/src/scripts/deep-sleep/apply_findings.py +168 -8
- package/src/scripts/deep-sleep/collect.py +33 -11
- package/src/scripts/deep-sleep/extract-prompt.md +38 -0
- package/src/scripts/deep-sleep/extract.py +80 -8
- package/src/scripts/deep-sleep/synthesize-prompt.md +59 -2
- package/src/scripts/deep-sleep/synthesize.py +3 -1
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +242 -0
- package/src/scripts/nexo-catchup.py +65 -29
- package/src/scripts/nexo-cognitive-decay 2.py +182 -0
- package/src/scripts/nexo-cron-wrapper.sh +53 -0
- package/src/scripts/nexo-daily-self-audit 2.py +552 -0
- package/src/scripts/nexo-daily-self-audit.py +4 -2
- package/src/scripts/nexo-deep-sleep 2.sh +97 -0
- package/src/scripts/nexo-deep-sleep.sh +66 -77
- package/src/scripts/nexo-evolution-run 2.py +597 -0
- package/src/scripts/nexo-evolution-run.py +13 -0
- package/src/scripts/nexo-followup-hygiene 2.py +112 -0
- package/src/scripts/nexo-immune 2.py +927 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +245 -0
- package/src/scripts/nexo-learning-housekeep.py +156 -1
- package/src/scripts/nexo-learning-validator 2.py +207 -0
- package/src/scripts/nexo-learning-validator.py +19 -0
- package/src/scripts/nexo-migrate 2.py +232 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +421 -0
- package/src/scripts/nexo-postmortem-consolidator.py +3 -2
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +29 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +345 -0
- package/src/scripts/nexo-reflection 2.py +253 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-send-email 2.py +25 -0
- package/src/scripts/nexo-send-reply 2.py +178 -0
- package/src/scripts/nexo-sleep 2.py +592 -0
- package/src/scripts/nexo-sleep.py +16 -11
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-synthesis 2.py +253 -0
- package/src/scripts/nexo-synthesis.py +46 -3
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +161 -0
- package/src/scripts/nexo-watchdog 2.sh +878 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/nexo-watchdog.sh +72 -19
- package/src/server 2.py +733 -0
- package/src/server.py +11 -2
- package/src/storage_router 2.py +32 -0
- package/src/tools_coordination 2.py +102 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_learnings 2.py +220 -0
- package/src/tools_menu 2.py +227 -0
- package/src/tools_reminders 2.py +86 -0
- package/src/tools_reminders_crud 2.py +159 -0
- package/src/tools_reminders_crud.py +7 -0
- package/src/tools_sessions 2.py +476 -0
- package/src/tools_task_history 2.py +57 -0
- package/templates/CLAUDE.md 2.template +63 -0
- package/templates/openclaw 2.json +13 -0
- package/tests/__init__ 2.py +0 -0
- package/tests/conftest 2.py +71 -0
- package/tests/test_cognitive 2.py +205 -0
- package/tests/test_knowledge_graph 2.py +140 -0
- package/tests/test_migrations 2.py +137 -0
- package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
- /package/src/scripts/{nexo-github-monitor.py → nexo-github-monitor 2.py} +0 -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))
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Context checker for NEXO operations - prevents duplicate actions.
|
|
3
|
+
|
|
4
|
+
Mechanical checks (email sent, file exists, action done) run in Python.
|
|
5
|
+
When the 'smart' command is used, passes context to Claude CLI for
|
|
6
|
+
intelligent duplicate/conflict detection that goes beyond file checks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import json
|
|
12
|
+
import hashlib
|
|
13
|
+
import subprocess
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
18
|
+
|
|
19
|
+
CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
|
|
20
|
+
|
|
21
|
+
class ContextChecker:
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.state_dir = NEXO_HOME / 'state'
|
|
24
|
+
self.state_dir.mkdir(exist_ok=True)
|
|
25
|
+
|
|
26
|
+
def check_email_sent(self, to_addr, subject, since_hours=72):
|
|
27
|
+
"""Check if email was already sent to address with subject."""
|
|
28
|
+
sent_path = Path.home() / 'mail' / '.nexo-sent' / '.Sent' # Configure for your mail setup
|
|
29
|
+
if not sent_path.exists():
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
subject_lower = subject.lower()
|
|
33
|
+
to_lower = to_addr.lower()
|
|
34
|
+
cutoff = datetime.now().timestamp() - (since_hours * 3600)
|
|
35
|
+
cur_dir = sent_path / 'cur'
|
|
36
|
+
if not cur_dir.exists():
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
for msg_file in cur_dir.iterdir():
|
|
40
|
+
try:
|
|
41
|
+
if msg_file.stat().st_mtime < cutoff:
|
|
42
|
+
continue
|
|
43
|
+
content = msg_file.read_text(errors='ignore')
|
|
44
|
+
except (OSError, UnicodeDecodeError):
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
content_lower = content.lower()
|
|
48
|
+
if f"to:{to_lower}" in content_lower or f"to: {to_lower}" in content_lower:
|
|
49
|
+
if subject_lower in content_lower:
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def check_file_exists(self, pattern, search_dirs=None):
|
|
54
|
+
"""Check if file matching pattern exists in common locations."""
|
|
55
|
+
if search_dirs is None:
|
|
56
|
+
search_dirs = [
|
|
57
|
+
'/var/www/vhosts',
|
|
58
|
+
str(NEXO_HOME),
|
|
59
|
+
'/opt'
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for base_dir in search_dirs:
|
|
63
|
+
if not os.path.exists(base_dir):
|
|
64
|
+
continue
|
|
65
|
+
matches = []
|
|
66
|
+
try:
|
|
67
|
+
for root, _, files in os.walk(base_dir):
|
|
68
|
+
for filename in files:
|
|
69
|
+
if pattern in filename:
|
|
70
|
+
matches.append(str(Path(root) / filename))
|
|
71
|
+
if len(matches) >= 5:
|
|
72
|
+
return matches
|
|
73
|
+
except OSError:
|
|
74
|
+
continue
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
def check_action_done(self, action_type, identifier, ttl_days=7):
|
|
78
|
+
"""Check if action was already performed recently."""
|
|
79
|
+
action_file = self.state_dir / 'actions.json'
|
|
80
|
+
|
|
81
|
+
# Load existing actions
|
|
82
|
+
actions = {}
|
|
83
|
+
if action_file.exists():
|
|
84
|
+
with open(action_file) as f:
|
|
85
|
+
actions = json.load(f)
|
|
86
|
+
|
|
87
|
+
# Create action key
|
|
88
|
+
key = hashlib.md5(f"{action_type}:{identifier}".encode()).hexdigest()
|
|
89
|
+
|
|
90
|
+
# Check if exists and not expired
|
|
91
|
+
if key in actions:
|
|
92
|
+
action_time = datetime.fromisoformat(actions[key]['timestamp'])
|
|
93
|
+
age_days = (datetime.now() - action_time).days
|
|
94
|
+
if age_days < ttl_days:
|
|
95
|
+
return True, actions[key]
|
|
96
|
+
|
|
97
|
+
return False, None
|
|
98
|
+
|
|
99
|
+
def mark_action_done(self, action_type, identifier, metadata=None):
|
|
100
|
+
"""Mark action as completed."""
|
|
101
|
+
action_file = self.state_dir / 'actions.json'
|
|
102
|
+
|
|
103
|
+
# Load existing actions
|
|
104
|
+
actions = {}
|
|
105
|
+
if action_file.exists():
|
|
106
|
+
with open(action_file) as f:
|
|
107
|
+
actions = json.load(f)
|
|
108
|
+
|
|
109
|
+
# Add new action
|
|
110
|
+
key = hashlib.md5(f"{action_type}:{identifier}".encode()).hexdigest()
|
|
111
|
+
actions[key] = {
|
|
112
|
+
'type': action_type,
|
|
113
|
+
'identifier': identifier,
|
|
114
|
+
'timestamp': datetime.now().isoformat(),
|
|
115
|
+
'metadata': metadata or {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Save
|
|
119
|
+
with open(action_file, 'w') as f:
|
|
120
|
+
json.dump(actions, f, indent=2)
|
|
121
|
+
|
|
122
|
+
return key
|
|
123
|
+
|
|
124
|
+
def smart_check(action_description: str, context: str = "") -> dict:
|
|
125
|
+
"""Use Claude CLI to intelligently check if an action would be redundant.
|
|
126
|
+
|
|
127
|
+
Goes beyond simple file/hash checks — understands intent and context
|
|
128
|
+
to detect semantic duplicates (e.g., "send welcome email" vs
|
|
129
|
+
"email onboarding message" to same person).
|
|
130
|
+
"""
|
|
131
|
+
checker = ContextChecker()
|
|
132
|
+
|
|
133
|
+
# Gather mechanical context first
|
|
134
|
+
state_file = checker.state_dir / 'actions.json'
|
|
135
|
+
recent_actions = {}
|
|
136
|
+
if state_file.exists():
|
|
137
|
+
try:
|
|
138
|
+
all_actions = json.loads(state_file.read_text())
|
|
139
|
+
cutoff = datetime.now().timestamp() - (7 * 86400)
|
|
140
|
+
for k, v in all_actions.items():
|
|
141
|
+
try:
|
|
142
|
+
ts = datetime.fromisoformat(v['timestamp']).timestamp()
|
|
143
|
+
if ts > cutoff:
|
|
144
|
+
recent_actions[k] = v
|
|
145
|
+
except (ValueError, KeyError):
|
|
146
|
+
pass
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
if not CLAUDE_CLI.exists():
|
|
151
|
+
return {"redundant": False, "reason": "CLI unavailable, cannot smart-check"}
|
|
152
|
+
|
|
153
|
+
prompt = f"""You are a context deduplication engine for NEXO operations.
|
|
154
|
+
|
|
155
|
+
PROPOSED ACTION:
|
|
156
|
+
{action_description}
|
|
157
|
+
|
|
158
|
+
ADDITIONAL CONTEXT:
|
|
159
|
+
{context or "None"}
|
|
160
|
+
|
|
161
|
+
RECENT ACTIONS (last 7 days):
|
|
162
|
+
{json.dumps(list(recent_actions.values()), indent=1, default=str)}
|
|
163
|
+
|
|
164
|
+
Respond with ONLY valid JSON (no markdown):
|
|
165
|
+
{{
|
|
166
|
+
"redundant": true/false,
|
|
167
|
+
"confidence": 0.0-1.0,
|
|
168
|
+
"reason": "<one line explanation>",
|
|
169
|
+
"matching_action": "<identifier of matching action if redundant, else null>"
|
|
170
|
+
}}
|
|
171
|
+
|
|
172
|
+
Rules:
|
|
173
|
+
- Same recipient + same intent within 72h = redundant
|
|
174
|
+
- Same file modification with same content = redundant
|
|
175
|
+
- Similar but different scope (e.g., different recipients) = NOT redundant
|
|
176
|
+
- When in doubt, say not redundant (false negatives are cheaper than false positives)"""
|
|
177
|
+
)
|
|
178
|
+
if auth_check.returncode != 0:
|
|
179
|
+
# CLI not authenticated, skip gracefully
|
|
180
|
+
return {"redundant": False, "reason": "CLI not authenticated — skipped analysis", "suggestion": "N/A"}
|
|
181
|
+
|
|
182
|
+
env = os.environ.copy()
|
|
183
|
+
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
184
|
+
env.pop("CLAUDECODE", None)
|
|
185
|
+
env.pop("CLAUDE_CODE", None)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
result = subprocess.run(
|
|
189
|
+
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text",
|
|
190
|
+
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
|
|
191
|
+
capture_output=True, text=True, timeout=21600, env=env
|
|
192
|
+
)
|
|
193
|
+
if result.returncode == 0:
|
|
194
|
+
text = result.stdout.strip()
|
|
195
|
+
if "```json" in text:
|
|
196
|
+
text = text.split("```json")[1].split("```")[0]
|
|
197
|
+
elif "```" in text:
|
|
198
|
+
text = text.split("```")[1].split("```")[0]
|
|
199
|
+
return json.loads(text.strip())
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
return {"redundant": False, "reason": "CLI check failed, defaulting to not redundant"}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def main():
|
|
207
|
+
"""CLI interface for context checking."""
|
|
208
|
+
if len(sys.argv) < 3:
|
|
209
|
+
print("Usage: check-context.py <command> <args>")
|
|
210
|
+
print("Commands:")
|
|
211
|
+
print(" email <to> <subject> - Check if email was sent")
|
|
212
|
+
print(" file <pattern> - Check if file exists")
|
|
213
|
+
print(" action <type> <id> - Check if action was done")
|
|
214
|
+
print(" smart <description> [ctx] - Intelligent duplicate check via CLI")
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
|
|
217
|
+
checker = ContextChecker()
|
|
218
|
+
command = sys.argv[1]
|
|
219
|
+
|
|
220
|
+
if command == 'email':
|
|
221
|
+
if len(sys.argv) < 4:
|
|
222
|
+
print("Usage: check-context.py email <to> <subject>")
|
|
223
|
+
sys.exit(1)
|
|
224
|
+
exists = checker.check_email_sent(sys.argv[2], sys.argv[3])
|
|
225
|
+
print("EXISTS" if exists else "NOT_FOUND")
|
|
226
|
+
sys.exit(0 if not exists else 1)
|
|
227
|
+
|
|
228
|
+
elif command == 'file':
|
|
229
|
+
files = checker.check_file_exists(sys.argv[2])
|
|
230
|
+
if files:
|
|
231
|
+
print("\n".join(files))
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
else:
|
|
234
|
+
print("NOT_FOUND")
|
|
235
|
+
sys.exit(0)
|
|
236
|
+
|
|
237
|
+
elif command == 'action':
|
|
238
|
+
if len(sys.argv) < 4:
|
|
239
|
+
print("Usage: check-context.py action <type> <id>")
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
done, data = checker.check_action_done(sys.argv[2], sys.argv[3])
|
|
242
|
+
if done:
|
|
243
|
+
print(f"DONE: {data}")
|
|
244
|
+
sys.exit(1)
|
|
245
|
+
else:
|
|
246
|
+
print("NOT_DONE")
|
|
247
|
+
sys.exit(0)
|
|
248
|
+
|
|
249
|
+
elif command == 'smart':
|
|
250
|
+
if len(sys.argv) < 3:
|
|
251
|
+
print("Usage: check-context.py smart <description> [context]")
|
|
252
|
+
sys.exit(1)
|
|
253
|
+
description = sys.argv[2]
|
|
254
|
+
context = sys.argv[3] if len(sys.argv) > 3 else ""
|
|
255
|
+
result = smart_check(description, context)
|
|
256
|
+
print(json.dumps(result, indent=2))
|
|
257
|
+
sys.exit(1 if result.get("redundant") else 0)
|
|
258
|
+
|
|
259
|
+
else:
|
|
260
|
+
print(f"Unknown command: {command}")
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
|
|
263
|
+
if __name__ == '__main__':
|
|
264
|
+
main()
|