nexo-brain 5.3.20 → 5.3.21
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/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/src/auto_update.py +11 -8
- package/src/dashboard/static/favicon 2.svg +32 -0
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +40 -0
- package/src/dashboard/static/style 2.css +2458 -0
- package/src/dashboard/templates/adaptive 2.html +118 -0
- package/src/dashboard/templates/artifacts 2.html +133 -0
- package/src/dashboard/templates/backups 2.html +136 -0
- package/src/dashboard/templates/base 2.html +417 -0
- package/src/dashboard/templates/calendar 2.html +591 -0
- package/src/dashboard/templates/chat 2.html +356 -0
- package/src/dashboard/templates/claims 2.html +259 -0
- package/src/dashboard/templates/cortex 2.html +321 -0
- package/src/dashboard/templates/credentials 2.html +128 -0
- package/src/dashboard/templates/crons 2.html +370 -0
- package/src/dashboard/templates/dashboard 2.html +494 -0
- package/src/dashboard/templates/dreams 2.html +252 -0
- package/src/dashboard/templates/email 2.html +160 -0
- package/src/dashboard/templates/evolution 2.html +189 -0
- package/src/dashboard/templates/feed 2.html +249 -0
- package/src/dashboard/templates/followup_health 2.html +170 -0
- package/src/dashboard/templates/graph 2.html +201 -0
- package/src/dashboard/templates/guard 2.html +259 -0
- package/src/dashboard/templates/inbox 2.html +251 -0
- package/src/dashboard/templates/memory 2.html +420 -0
- package/src/dashboard/templates/operations 2.html +608 -0
- package/src/dashboard/templates/plugins 2.html +185 -0
- package/src/dashboard/templates/protocol 2.html +199 -0
- package/src/dashboard/templates/rules 2.html +246 -0
- package/src/dashboard/templates/sentiment 2.html +247 -0
- package/src/dashboard/templates/sessions 2.html +218 -0
- package/src/dashboard/templates/skills 2.html +329 -0
- package/src/dashboard/templates/somatic 2.html +73 -0
- package/src/dashboard/templates/triggers 2.html +133 -0
- package/src/dashboard/templates/trust 2.html +360 -0
- package/src/db/__init__ 2.py +259 -0
- package/src/db/_core 2.py +437 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_episodic 2.py +762 -0
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_goal_profiles 2.py +376 -0
- package/src/db/_hot_context 2.py +660 -0
- package/src/db/_outcomes 2.py +800 -0
- package/src/db/_personal_scripts 2.py +582 -0
- package/src/db/_sessions 2.py +330 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/db/_watchers 2.py +173 -0
- package/src/doctor/formatters 2.py +52 -0
- package/src/doctor/models 2.py +69 -0
- package/src/doctor/planes 2.py +87 -0
- package/src/doctor/providers/__init__ 2.py +1 -0
- package/src/doctor/providers/deep 2.py +367 -0
- package/src/evolution_cycle 2.py +519 -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-tool-logs 2.sh +158 -0
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/heartbeat-enforcement 2.py +90 -0
- package/src/hooks/heartbeat-posttool 2.sh +18 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/post-compact 2.sh +152 -0
- package/src/hooks/pre-compact 2.sh +169 -0
- package/src/hooks/protocol-guardrail 2.sh +10 -0
- package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
- package/src/hooks/session-stop 2.sh +52 -0
- package/src/kg_populate 2.py +292 -0
- package/src/maintenance 2.py +53 -0
- package/src/memory_backends 2.py +71 -0
- package/src/migrate_embeddings 2.py +124 -0
- package/src/nexo_sdk 2.py +103 -0
- package/src/observability 2.py +199 -0
- package/src/plugin_loader 2.py +217 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +127 -0
- package/src/plugins/claims_tools 2.py +119 -0
- package/src/plugins/cognitive_memory 2.py +609 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +1155 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +560 -0
- package/src/plugins/evolution 2.py +167 -0
- package/src/plugins/goal_engine 2.py +142 -0
- package/src/plugins/guard 2.py +862 -0
- package/src/plugins/impact 2.py +29 -0
- package/src/plugins/knowledge_graph_tools 2.py +137 -0
- package/src/plugins/media_memory_tools 2.py +98 -0
- package/src/plugins/memory_export 2.py +196 -0
- package/src/plugins/outcomes 2.py +130 -0
- package/src/plugins/personal_scripts 2.py +117 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/protocol 2.py +1449 -0
- package/src/plugins/simple_api 2.py +106 -0
- package/src/plugins/skills 2.py +341 -0
- package/src/plugins/state_watchers 2.py +79 -0
- package/src/plugins/update 2.py +986 -0
- package/src/plugins/user_state_tools 2.py +43 -0
- package/src/plugins/workflow 2.py +588 -0
- package/src/protocol_settings 2.py +59 -0
- package/src/public_contribution 2.py +466 -0
- package/src/public_evolution_queue 2.py +241 -0
- package/src/requirements 2.txt +14 -0
- package/src/retroactive_learnings 2.py +373 -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/runtime_power 2.py +874 -0
- package/src/script_registry 2.py +1559 -0
- package/src/scripts/check-context 2.py +272 -0
- package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
- package/src/scripts/deep-sleep/collect 2.py +928 -0
- package/src/scripts/deep-sleep/extract 2.py +330 -0
- package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
- package/src/scripts/deep-sleep/synthesize 2.py +312 -0
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
- package/src/scripts/nexo-agent-run 2.py +75 -0
- 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 +300 -0
- package/src/scripts/nexo-cognitive-decay 2.py +257 -0
- package/src/scripts/nexo-cortex-cycle 2.py +293 -0
- package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
- package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
- package/src/scripts/nexo-dashboard 2.sh +29 -0
- package/src/scripts/nexo-deep-sleep 2.sh +86 -0
- package/src/scripts/nexo-evolution-run 2.py +1664 -0
- package/src/scripts/nexo-followup-hygiene 2.py +139 -0
- package/src/scripts/nexo-hook-record 2.py +42 -0
- package/src/scripts/nexo-immune 2.py +936 -0
- package/src/scripts/nexo-impact-scorer 2.py +117 -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 +401 -0
- package/src/scripts/nexo-learning-validator 2.py +266 -0
- package/src/scripts/nexo-migrate 2.py +260 -0
- package/src/scripts/nexo-outcome-checker 2.py +127 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
- package/src/scripts/nexo-reflection 2.py +256 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-sleep 2.py +631 -0
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-sync-clients 2.py +16 -0
- package/src/scripts/nexo-synthesis 2.py +475 -0
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +306 -0
- package/src/scripts/nexo-watchdog 2.sh +1207 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
- package/src/server 2.py +1296 -0
- package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
- package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
- package/src/skills/run-release-final-audit/guide 2.md +16 -0
- package/src/skills/run-release-final-audit/script 2.py +259 -0
- package/src/skills/run-release-final-audit/skill 2.json +77 -0
- package/src/skills/run-runtime-doctor/guide 2.md +12 -0
- package/src/skills/run-runtime-doctor/script 2.py +21 -0
- package/src/skills/run-runtime-doctor/skill 2.json +25 -0
- package/src/skills_runtime 2.py +932 -0
- package/src/state_watchers_runtime 2.py +475 -0
- package/src/storage_router 2.py +32 -0
- package/src/system_catalog 2.py +786 -0
- package/src/tools_coordination 2.py +103 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_drive 2.py +487 -0
- package/src/tools_hot_context 2.py +163 -0
- package/src/tools_learnings 2.py +612 -0
- package/src/tools_menu 2.py +229 -0
- package/src/tools_reminders 2.py +88 -0
- package/src/tools_reminders_crud 2.py +363 -0
- package/src/tools_sessions 2.py +1054 -0
- package/src/tools_system_catalog 2.py +19 -0
- package/src/tools_task_history 2.py +57 -0
- package/src/tools_transcripts 2.py +98 -0
- package/src/transcript_utils 2.py +412 -0
- package/src/user_context 2.py +46 -0
- package/src/user_data_portability 2.py +328 -0
- package/src/user_state_model 2.py +170 -0
- package/templates/CLAUDE.md 2.template +108 -0
- package/templates/CODEX.AGENTS.md 2.template +66 -0
- package/templates/launchagents/README 2.md +132 -0
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
- package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
- package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
- package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
- package/templates/launchagents/com.nexo.immune 2.plist +41 -0
- package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
- package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
- package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
- package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
- package/templates/nexo_helper 2.py +301 -0
- package/templates/openclaw 2.json +13 -0
- package/templates/plugin-template 2.py +40 -0
- package/templates/script-template 2.py +59 -0
- package/templates/script-template 2.sh +13 -0
- package/templates/skill-script-template 2.py +48 -0
- package/templates/skill-template 2.md +33 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NEXO Post-Mortem Consolidator v2 — The brain consolidates memories.
|
|
4
|
+
|
|
5
|
+
Before: 595 lines of word-overlap at 50% to detect "patterns".
|
|
6
|
+
Now: Collects data, passes them to CLI which UNDERSTANDS what it reads.
|
|
7
|
+
|
|
8
|
+
Runs daily at 23:30 via LaunchAgent. Reads session diaries from today,
|
|
9
|
+
passes them to the configured automation backend, which decides what deserves permanent memory.
|
|
10
|
+
|
|
11
|
+
Stage 1 — Data collection (Pure Python):
|
|
12
|
+
Query session diaries, existing feedbacks, history.
|
|
13
|
+
|
|
14
|
+
Stage 2 — Intelligence (automation backend):
|
|
15
|
+
Read diaries, understand patterns, decide what to promote.
|
|
16
|
+
|
|
17
|
+
Stage 3 — Sensory Register + Force analysis (Pure Python):
|
|
18
|
+
Process cognitive events. Kept from v1 — genuinely mechanical.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sqlite3
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
from datetime import datetime, date, timedelta
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
HOME = Path.home()
|
|
30
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(HOME / ".nexo")))
|
|
31
|
+
|
|
32
|
+
# Add NEXO_HOME to path for cognitive engine (Stage 3)
|
|
33
|
+
# Auto-detect: if running from repo (src/scripts/), use src/ as NEXO_CODE
|
|
34
|
+
_script_dir = Path(__file__).resolve().parent
|
|
35
|
+
_repo_src = _script_dir.parent # src/scripts/ -> src/
|
|
36
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
|
|
37
|
+
sys.path.insert(0, str(NEXO_CODE))
|
|
38
|
+
|
|
39
|
+
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
from client_preferences import resolve_user_model as _resolve_user_model
|
|
43
|
+
_USER_MODEL = _resolve_user_model()
|
|
44
|
+
except Exception:
|
|
45
|
+
_USER_MODEL = ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
49
|
+
# Memory directory — adjust to match your project's memory location
|
|
50
|
+
MEMORY_DIR = NEXO_HOME / "memory"
|
|
51
|
+
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
|
|
52
|
+
HISTORY_FILE = NEXO_HOME / "coordination" / "postmortem-history.json"
|
|
53
|
+
CONSOLIDATION_LOG = NEXO_HOME / "logs" / "postmortem-consolidation.log"
|
|
54
|
+
def _resolve_claude_cli() -> Path:
|
|
55
|
+
"""Find claude CLI: saved path > PATH > common locations."""
|
|
56
|
+
import shutil as _shutil
|
|
57
|
+
saved = NEXO_HOME / "config" / "claude-cli-path"
|
|
58
|
+
if saved.exists():
|
|
59
|
+
p = Path(saved.read_text().strip())
|
|
60
|
+
if p.exists():
|
|
61
|
+
return p
|
|
62
|
+
found = _shutil.which("claude")
|
|
63
|
+
if found:
|
|
64
|
+
return Path(found)
|
|
65
|
+
for candidate in [
|
|
66
|
+
HOME / ".local" / "bin" / "claude",
|
|
67
|
+
HOME / ".npm-global" / "bin" / "claude",
|
|
68
|
+
Path("/usr/local/bin/claude"),
|
|
69
|
+
]:
|
|
70
|
+
if candidate.exists():
|
|
71
|
+
return candidate
|
|
72
|
+
return HOME / ".local" / "bin" / "claude"
|
|
73
|
+
|
|
74
|
+
CLAUDE_CLI = _resolve_claude_cli()
|
|
75
|
+
SESSION_BUFFER = NEXO_HOME / "brain" / "session_buffer.jsonl"
|
|
76
|
+
|
|
77
|
+
TODAY = date.today()
|
|
78
|
+
TODAY_STR = TODAY.isoformat()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def log(msg: str):
|
|
82
|
+
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
83
|
+
line = f"[{ts}] {msg}"
|
|
84
|
+
print(line, flush=True)
|
|
85
|
+
CONSOLIDATION_LOG.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
with open(CONSOLIDATION_LOG, "a") as f:
|
|
87
|
+
f.write(line + "\n")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ─── Stage 1: Data Collection (Pure Python) ─────────────────────────────────
|
|
91
|
+
|
|
92
|
+
def collect_data() -> dict:
|
|
93
|
+
"""Collects all data the CLI will need to decide."""
|
|
94
|
+
data = {
|
|
95
|
+
"date": TODAY_STR,
|
|
96
|
+
"diaries": [],
|
|
97
|
+
"existing_feedbacks": [],
|
|
98
|
+
"history_summary": {},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if not NEXO_DB.exists():
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
105
|
+
conn.row_factory = sqlite3.Row
|
|
106
|
+
|
|
107
|
+
# Today's diaries with self-critique
|
|
108
|
+
rows = conn.execute(
|
|
109
|
+
"SELECT id, session_id, summary, self_critique, user_signals, "
|
|
110
|
+
"mental_state, domain, created_at "
|
|
111
|
+
"FROM session_diary WHERE date(created_at) = ? ORDER BY created_at",
|
|
112
|
+
(TODAY_STR,)
|
|
113
|
+
).fetchall()
|
|
114
|
+
data["diaries"] = [dict(r) for r in rows]
|
|
115
|
+
|
|
116
|
+
conn.close()
|
|
117
|
+
|
|
118
|
+
# Existing postmortem feedbacks (nombres, para no duplicar)
|
|
119
|
+
data["existing_feedbacks"] = [
|
|
120
|
+
f.stem for f in MEMORY_DIR.glob("feedback_postmortem_*.md")
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# History summary
|
|
124
|
+
if HISTORY_FILE.exists():
|
|
125
|
+
try:
|
|
126
|
+
history = json.loads(HISTORY_FILE.read_text())
|
|
127
|
+
data["history_summary"] = {
|
|
128
|
+
"total_permanent_rules": len(history.get("permanent_rules", [])),
|
|
129
|
+
"days_tracked": len(history.get("days", {})),
|
|
130
|
+
"recent_rules": history.get("permanent_rules", [])[-10:],
|
|
131
|
+
}
|
|
132
|
+
except Exception:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
return data
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ─── Stage 2: Intelligence (automation backend) ─────────────────────────────
|
|
139
|
+
|
|
140
|
+
def consolidate_with_cli(data: dict) -> bool:
|
|
141
|
+
"""The brain consolidates — CLI decides what to promote."""
|
|
142
|
+
|
|
143
|
+
diaries_with_critique = [
|
|
144
|
+
d for d in data["diaries"]
|
|
145
|
+
if d.get("self_critique") and not str(d["self_critique"]).strip().lower().startswith("no self-critique")
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if not diaries_with_critique:
|
|
149
|
+
log("All sessions clean or trivial. Nothing to consolidate.")
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
# Prepare data for CLI (truncate to avoid exceeding context)
|
|
153
|
+
diaries_json = json.dumps(diaries_with_critique, ensure_ascii=False, indent=1)
|
|
154
|
+
if len(diaries_json) > 12000:
|
|
155
|
+
diaries_json = diaries_json[:12000] + "\n... (truncated)"
|
|
156
|
+
|
|
157
|
+
prompt = f"""FIRST: Call nexo_startup(task='nightly postmortem consolidation') to register this session.
|
|
158
|
+
|
|
159
|
+
You are NEXO's nightly consolidator. Your job is to review the self-critiques
|
|
160
|
+
from today and decide which deserve to become permanent rules. Use nexo_learning_add for permanent rules and nexo_followup_create for action items.
|
|
161
|
+
|
|
162
|
+
DATE: {data['date']}
|
|
163
|
+
SESSIONS TODAY: {len(data['diaries'])} total, {len(diaries_with_critique)} with self-critique
|
|
164
|
+
|
|
165
|
+
DIARIES WITH SELF-CRITIQUE:
|
|
166
|
+
{diaries_json}
|
|
167
|
+
|
|
168
|
+
EXISTING POSTMORTEM FEEDBACKS ({len(data['existing_feedbacks'])}):
|
|
169
|
+
{json.dumps(data['existing_feedbacks'][:30], ensure_ascii=False)}
|
|
170
|
+
|
|
171
|
+
RECENT PERMANENT RULES:
|
|
172
|
+
{json.dumps(data['history_summary'].get('recent_rules', []), ensure_ascii=False)}
|
|
173
|
+
|
|
174
|
+
INSTRUCTIONS:
|
|
175
|
+
|
|
176
|
+
1. Read each self_critique and understand its MEANING (don't count words).
|
|
177
|
+
|
|
178
|
+
2. PROMOTE to permanent feedback ONLY IF:
|
|
179
|
+
- A pattern appears in 2+ different sessions of the day (by meaning, not literal text)
|
|
180
|
+
- Or the user explicitly corrected (user_signals contains correction)
|
|
181
|
+
- And the self-critique contains a CONCRETE ACTION that prevents a future error
|
|
182
|
+
- And a similar feedback does NOT already exist in the existing ones
|
|
183
|
+
|
|
184
|
+
3. DO NOT promote if:
|
|
185
|
+
- It's a negative response ("Nothing happened", "clean session")
|
|
186
|
+
- It's generic without concrete action
|
|
187
|
+
- A feedback covering the same topic already exists
|
|
188
|
+
|
|
189
|
+
4. For each rule to promote, create the file with Write en {MEMORY_DIR}/:
|
|
190
|
+
Nombre: feedback_postmortem_[slug_descriptivo].md
|
|
191
|
+
Formato:
|
|
192
|
+
---
|
|
193
|
+
name: [descriptive title]
|
|
194
|
+
description: Behavioral rule extracted from self-critique — recurring pattern
|
|
195
|
+
type: feedback
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
[Clear description of the pattern and rule]
|
|
199
|
+
|
|
200
|
+
**Why:** [Why this matters — with evidence from sessions]
|
|
201
|
+
**How to apply:** [When and how to apply this rule]
|
|
202
|
+
|
|
203
|
+
5. Write the daily summary en $NEXO_HOME/coordination/postmortem-daily.md:
|
|
204
|
+
# Post-Mortem Daily — {data['date']}
|
|
205
|
+
Sessions: X | Self-critiques: Y | Promoted: Z
|
|
206
|
+
|
|
207
|
+
## Today's self-critiques (summary)
|
|
208
|
+
[Lista breve]
|
|
209
|
+
|
|
210
|
+
## Promoted to permanent memory
|
|
211
|
+
[What you promoted and why]
|
|
212
|
+
|
|
213
|
+
## Discarded (and why)
|
|
214
|
+
[What you did NOT promote and the reason]
|
|
215
|
+
|
|
216
|
+
Execute without asking."""
|
|
217
|
+
|
|
218
|
+
log(f"Stage 2: Invoking automation backend with {len(diaries_with_critique)} critiques...")
|
|
219
|
+
try:
|
|
220
|
+
result = run_automation_prompt(
|
|
221
|
+
prompt,
|
|
222
|
+
model=_USER_MODEL or "opus",
|
|
223
|
+
timeout=21600,
|
|
224
|
+
output_format="text",
|
|
225
|
+
allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if result.returncode != 0:
|
|
229
|
+
log(f"Stage 2: CLI error (code {result.returncode}): {(result.stderr or '')[:300]}")
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
log(f"Stage 2: Completed. Output: {len(result.stdout or '')} chars")
|
|
233
|
+
# Log last 500 chars of output for debugging
|
|
234
|
+
if result.stdout:
|
|
235
|
+
log(f"Stage 2 output tail: {result.stdout[-500:]}")
|
|
236
|
+
return True
|
|
237
|
+
|
|
238
|
+
except AutomationBackendUnavailableError as e:
|
|
239
|
+
log(f"Stage 2: automation backend unavailable: {e}")
|
|
240
|
+
return False
|
|
241
|
+
except subprocess.TimeoutExpired:
|
|
242
|
+
log("Stage 2: CLI timed out (300s)")
|
|
243
|
+
return False
|
|
244
|
+
except Exception as e:
|
|
245
|
+
log(f"Stage 2: Exception: {e}")
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ─── Stage 3: Sensory Register + Force Analysis (Pure Python) ───────────────
|
|
250
|
+
# Kept from v1 — these are genuinely mechanical (embedding vectors, DB updates)
|
|
251
|
+
|
|
252
|
+
def process_sensory_register():
|
|
253
|
+
"""Sensory Register — Atkinson-Shiffrin Layer 1. Embeds events into STM."""
|
|
254
|
+
log("--- Sensory Register processing ---")
|
|
255
|
+
|
|
256
|
+
if not SESSION_BUFFER.exists():
|
|
257
|
+
log(" No session_buffer.jsonl found, skipping")
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
today_events = []
|
|
261
|
+
try:
|
|
262
|
+
with open(SESSION_BUFFER) as f:
|
|
263
|
+
for line in f:
|
|
264
|
+
line = line.strip()
|
|
265
|
+
if not line:
|
|
266
|
+
continue
|
|
267
|
+
try:
|
|
268
|
+
event = json.loads(line)
|
|
269
|
+
if event.get("ts", "").startswith(TODAY_STR):
|
|
270
|
+
today_events.append(event)
|
|
271
|
+
except json.JSONDecodeError:
|
|
272
|
+
continue
|
|
273
|
+
except Exception as e:
|
|
274
|
+
log(f" Error reading session_buffer: {e}")
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
if not today_events:
|
|
278
|
+
log(" No events from today")
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
log(f" Found {len(today_events)} events")
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
import cognitive
|
|
285
|
+
except ImportError as e:
|
|
286
|
+
log(f" Cannot import cognitive: {e}")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
ingested = 0
|
|
290
|
+
for event in today_events:
|
|
291
|
+
source = event.get("source", "")
|
|
292
|
+
if source == "hook-fallback":
|
|
293
|
+
task_str = " ".join(event.get("tasks", []))
|
|
294
|
+
if len(task_str) < 50 or "," in task_str:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
parts = []
|
|
298
|
+
for key, label in [("tasks", "Tasks"), ("decisions", "Decisions"),
|
|
299
|
+
("errors_resolved", "Errors"), ("user_patterns", "the user")]:
|
|
300
|
+
val = event.get(key, [])
|
|
301
|
+
if val:
|
|
302
|
+
parts.append(f"{label}: {'; '.join(str(v) for v in val[:3])}")
|
|
303
|
+
|
|
304
|
+
critique = event.get("self_critique", "")
|
|
305
|
+
if critique and "hook-fallback" not in critique:
|
|
306
|
+
parts.append(f"Self-critique: {critique[:200]}")
|
|
307
|
+
|
|
308
|
+
content = " | ".join(parts)
|
|
309
|
+
if not content or len(content) < 20:
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
vec = cognitive.embed(content)
|
|
314
|
+
domain = ""
|
|
315
|
+
lower = content.lower()
|
|
316
|
+
# Add your project keywords for domain detection
|
|
317
|
+
for keyword, dom in [("nexo", "nexo")]:
|
|
318
|
+
if keyword in lower:
|
|
319
|
+
domain = dom
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
cognitive.ingest_sensory(
|
|
323
|
+
content=content, source_id=f"buffer#{event.get('ts', '')}",
|
|
324
|
+
domain=domain, created_at=event.get("ts", "")
|
|
325
|
+
)
|
|
326
|
+
ingested += 1
|
|
327
|
+
except Exception as e:
|
|
328
|
+
log(f" Error embedding: {e}")
|
|
329
|
+
|
|
330
|
+
log(f" Ingested {ingested} sensory events into STM")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def analyze_force_events():
|
|
334
|
+
"""Analyze --force dissonance resolutions from today."""
|
|
335
|
+
log("--- Force event analysis ---")
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
import cognitive
|
|
339
|
+
except ImportError:
|
|
340
|
+
log(" Cannot import cognitive, skipping")
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
db = cognitive._get_db()
|
|
344
|
+
today_forces = db.execute(
|
|
345
|
+
"""SELECT memory_id, context, created_at FROM memory_corrections
|
|
346
|
+
WHERE correction_type = 'exception' AND context LIKE '%[FORCE]%'
|
|
347
|
+
AND date(created_at) = ? ORDER BY created_at""",
|
|
348
|
+
(TODAY_STR,)
|
|
349
|
+
).fetchall()
|
|
350
|
+
|
|
351
|
+
if not today_forces:
|
|
352
|
+
log(" No --force events today")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
log(f" {len(today_forces)} --force events")
|
|
356
|
+
|
|
357
|
+
from collections import Counter
|
|
358
|
+
|
|
359
|
+
memory_counts = Counter(r["memory_id"] for r in today_forces)
|
|
360
|
+
for mem_id, count in memory_counts.most_common():
|
|
361
|
+
mem = db.execute(
|
|
362
|
+
"SELECT content, strength FROM ltm_memories WHERE id = ?", (mem_id,)
|
|
363
|
+
).fetchone()
|
|
364
|
+
if not mem:
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
total = db.execute(
|
|
368
|
+
"SELECT COUNT(*) FROM memory_corrections WHERE memory_id = ? AND context LIKE '%[FORCE]%'",
|
|
369
|
+
(mem_id,)
|
|
370
|
+
).fetchone()[0]
|
|
371
|
+
|
|
372
|
+
if total >= 3:
|
|
373
|
+
log(f" PARADIGM SHIFT: LTM #{mem_id} overridden {total}x → decay to 0.3")
|
|
374
|
+
db.execute(
|
|
375
|
+
"UPDATE ltm_memories SET strength = 0.3, "
|
|
376
|
+
"tags = CASE WHEN tags LIKE '%paradigm_candidate%' THEN tags "
|
|
377
|
+
"ELSE tags || ',paradigm_candidate' END WHERE id = ?",
|
|
378
|
+
(mem_id,)
|
|
379
|
+
)
|
|
380
|
+
elif count >= 2:
|
|
381
|
+
log(f" WATCH: LTM #{mem_id} overridden {count}x today")
|
|
382
|
+
|
|
383
|
+
db.commit()
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ─── Main ────────────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
def already_ran_today() -> bool:
|
|
389
|
+
"""Prevent running twice on the same day."""
|
|
390
|
+
marker = NEXO_HOME / "coordination" / "postmortem-last-run"
|
|
391
|
+
if marker.exists():
|
|
392
|
+
try:
|
|
393
|
+
return marker.read_text().strip() == TODAY_STR
|
|
394
|
+
except Exception:
|
|
395
|
+
return False
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def mark_done():
|
|
400
|
+
marker = NEXO_HOME / "coordination" / "postmortem-last-run"
|
|
401
|
+
marker.parent.mkdir(parents=True, exist_ok=True)
|
|
402
|
+
marker.write_text(TODAY_STR)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def main():
|
|
406
|
+
if already_ran_today():
|
|
407
|
+
log("Already ran today. Skipping.")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
log("=== NEXO Post-Mortem Consolidator v2 starting ===")
|
|
411
|
+
had_errors = False
|
|
412
|
+
|
|
413
|
+
# Stage 1: Collect data
|
|
414
|
+
data = collect_data()
|
|
415
|
+
log(f"Stage 1: {len(data['diaries'])} diaries, {len(data['existing_feedbacks'])} existing feedbacks")
|
|
416
|
+
|
|
417
|
+
if not data["diaries"]:
|
|
418
|
+
log("No session diaries today. Nothing to consolidate.")
|
|
419
|
+
else:
|
|
420
|
+
# Stage 2: CLI intelligence (graceful fallback: Stage 3 still runs)
|
|
421
|
+
success = consolidate_with_cli(data)
|
|
422
|
+
if not success:
|
|
423
|
+
log("Stage 2 failed (CLI unavailable or error). "
|
|
424
|
+
"Skipping intelligent consolidation. Stage 3 (sensory + force) will still run.")
|
|
425
|
+
had_errors = True
|
|
426
|
+
|
|
427
|
+
# Stage 3: Sensory Register (mechanical, kept from v1)
|
|
428
|
+
try:
|
|
429
|
+
process_sensory_register()
|
|
430
|
+
except Exception as e:
|
|
431
|
+
log(f"Sensory register failed: {e}")
|
|
432
|
+
had_errors = True
|
|
433
|
+
|
|
434
|
+
# Stage 3b: Force analysis (mechanical, kept from v1)
|
|
435
|
+
try:
|
|
436
|
+
analyze_force_events()
|
|
437
|
+
except Exception as e:
|
|
438
|
+
log(f"Force analysis failed: {e}")
|
|
439
|
+
had_errors = True
|
|
440
|
+
|
|
441
|
+
# Register successful run only if no stages failed
|
|
442
|
+
if not had_errors:
|
|
443
|
+
try:
|
|
444
|
+
state_file = NEXO_HOME / "operations" / ".catchup-state.json"
|
|
445
|
+
state = json.loads(state_file.read_text()) if state_file.exists() else {}
|
|
446
|
+
state["postmortem"] = datetime.now().isoformat()
|
|
447
|
+
state_file.write_text(json.dumps(state, indent=2))
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
|
|
451
|
+
mark_done()
|
|
452
|
+
log("=== Consolidation v2 complete ===")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
if __name__ == "__main__":
|
|
456
|
+
main()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env /Library/Frameworks/Python.framework/Versions/3.12/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
NEXO Pre-Commit Validator — Dynamic version
|
|
4
|
+
Queries nexo.db learnings to surface relevant warnings/blockers before commit.
|
|
5
|
+
Runs standalone (no MCP dependency, just sqlite3).
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import sqlite3
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
14
|
+
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_staged_files():
|
|
18
|
+
"""Get list of staged files."""
|
|
19
|
+
result = subprocess.run(
|
|
20
|
+
['git', 'diff', '--cached', '--name-only'],
|
|
21
|
+
capture_output=True, text=True
|
|
22
|
+
)
|
|
23
|
+
return [f for f in result.stdout.strip().split('\n') if f]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check_file(filepath, conn):
|
|
27
|
+
"""Dynamic checks from learnings DB."""
|
|
28
|
+
errors, warnings = [], []
|
|
29
|
+
|
|
30
|
+
filename = Path(filepath).name
|
|
31
|
+
parent_dir = Path(filepath).parent.name
|
|
32
|
+
|
|
33
|
+
# 1. Find learnings mentioning this file or directory
|
|
34
|
+
file_learnings = conn.execute(
|
|
35
|
+
"SELECT id, title, content FROM learnings WHERE INSTR(content, ?) > 0 OR INSTR(content, ?) > 0",
|
|
36
|
+
(filename, parent_dir)
|
|
37
|
+
).fetchall()
|
|
38
|
+
|
|
39
|
+
# 2. Check repetition count for each matching learning
|
|
40
|
+
for row in file_learnings:
|
|
41
|
+
lid, title = row[0], row[1]
|
|
42
|
+
rep_count = conn.execute(
|
|
43
|
+
"SELECT COUNT(*) FROM error_repetitions WHERE original_learning_id = ?",
|
|
44
|
+
(lid,)
|
|
45
|
+
).fetchone()[0]
|
|
46
|
+
|
|
47
|
+
if rep_count >= 5:
|
|
48
|
+
errors.append(f"BLOCKED #{lid} ({rep_count}x repeated): {title[:80]}")
|
|
49
|
+
elif rep_count >= 3:
|
|
50
|
+
warnings.append(f"WARNING #{lid} ({rep_count}x repeated): {title[:80]}")
|
|
51
|
+
|
|
52
|
+
# 3. Universal rules (SIEMPRE/NUNCA/ANTES) matching this file
|
|
53
|
+
universal = conn.execute(
|
|
54
|
+
"SELECT id, title, content FROM learnings WHERE "
|
|
55
|
+
"(content LIKE '%SIEMPRE%' OR content LIKE '%NUNCA%' OR content LIKE '%ANTES%') "
|
|
56
|
+
"AND (INSTR(content, ?) > 0 OR INSTR(content, ?) > 0)",
|
|
57
|
+
(filename, parent_dir)
|
|
58
|
+
).fetchall()
|
|
59
|
+
|
|
60
|
+
for row in universal:
|
|
61
|
+
lid, title = row[0], row[1]
|
|
62
|
+
# Don't duplicate if already found above
|
|
63
|
+
if not any(f"#{lid}" in e for e in errors + warnings):
|
|
64
|
+
warnings.append(f"RULE #{lid}: {title[:80]}")
|
|
65
|
+
|
|
66
|
+
return errors, warnings
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main():
|
|
70
|
+
"""Run pre-commit validation."""
|
|
71
|
+
staged = get_staged_files()
|
|
72
|
+
if not staged:
|
|
73
|
+
print("No staged files to check")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
if not NEXO_DB.exists():
|
|
77
|
+
print("nexo.db not found — skipping dynamic checks")
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
conn = sqlite3.connect(str(NEXO_DB), timeout=5)
|
|
81
|
+
|
|
82
|
+
all_errors = {}
|
|
83
|
+
all_warnings = {}
|
|
84
|
+
|
|
85
|
+
for filepath in staged:
|
|
86
|
+
errors, warnings = check_file(filepath, conn)
|
|
87
|
+
if errors:
|
|
88
|
+
all_errors[filepath] = errors
|
|
89
|
+
if warnings:
|
|
90
|
+
all_warnings[filepath] = warnings
|
|
91
|
+
|
|
92
|
+
conn.close()
|
|
93
|
+
|
|
94
|
+
# Print warnings (non-blocking)
|
|
95
|
+
if all_warnings:
|
|
96
|
+
print("\nWARNINGS:")
|
|
97
|
+
for filepath, warns in all_warnings.items():
|
|
98
|
+
print(f"\n {filepath}:")
|
|
99
|
+
for w in warns:
|
|
100
|
+
print(f" - {w}")
|
|
101
|
+
|
|
102
|
+
# Print errors (blocking)
|
|
103
|
+
if all_errors:
|
|
104
|
+
print("\nBLOCKED — Fix these before committing:\n")
|
|
105
|
+
for filepath, errs in all_errors.items():
|
|
106
|
+
print(f" {filepath}:")
|
|
107
|
+
for e in errs:
|
|
108
|
+
print(f" - {e}")
|
|
109
|
+
print()
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
if all_warnings:
|
|
113
|
+
print("\nPre-commit passed with warnings\n")
|
|
114
|
+
else:
|
|
115
|
+
print("Pre-commit validation passed")
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == '__main__':
|
|
120
|
+
sys.exit(main())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# NEXO Prevent Sleep — keeps the machine awake so nocturnal processes run.
|
|
3
|
+
#
|
|
4
|
+
# macOS: uses native /usr/bin/caffeinate for best-effort background availability
|
|
5
|
+
# Linux: uses systemd-inhibit or caffeine if available, otherwise no-op
|
|
6
|
+
#
|
|
7
|
+
# Run as LaunchAgent (KeepAlive) or systemd service.
|
|
8
|
+
|
|
9
|
+
case "$(uname -s)" in
|
|
10
|
+
Darwin)
|
|
11
|
+
if [[ ! -x /usr/bin/caffeinate ]]; then
|
|
12
|
+
echo "[NEXO] /usr/bin/caffeinate not found. macOS power helper unavailable."
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
# Keep the helper alive as long as this service runs. On laptops with the
|
|
16
|
+
# lid closed, actual behavior still depends on hardware and OS policy.
|
|
17
|
+
exec /usr/bin/caffeinate -d -i -m -s /bin/bash -lc 'while :; do sleep 3600; done'
|
|
18
|
+
;;
|
|
19
|
+
Linux)
|
|
20
|
+
if command -v systemd-inhibit &>/dev/null; then
|
|
21
|
+
exec systemd-inhibit --what=idle:sleep --who=nexo --why="NEXO nocturnal processes" sleep infinity
|
|
22
|
+
elif command -v caffeine &>/dev/null; then
|
|
23
|
+
exec caffeine
|
|
24
|
+
else
|
|
25
|
+
echo "[NEXO] No sleep prevention tool found. Install systemd-inhibit or caffeine."
|
|
26
|
+
echo "[NEXO] Nocturnal processes may not run if the system sleeps."
|
|
27
|
+
# Keep running so launchd/systemd doesn't restart loop
|
|
28
|
+
exec sleep infinity
|
|
29
|
+
fi
|
|
30
|
+
;;
|
|
31
|
+
*)
|
|
32
|
+
echo "[NEXO] Unsupported platform: $(uname -s)"
|
|
33
|
+
exit 1
|
|
34
|
+
;;
|
|
35
|
+
esac
|