nexo-brain 5.3.13 → 5.3.15

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.
Files changed (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/nexo-brain.js +52 -1
  3. package/package.json +1 -1
  4. package/src/crons/sync.py +18 -4
  5. package/src/dashboard/static/favicon 2.svg +32 -0
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +40 -0
  8. package/src/dashboard/static/style 2.css +2458 -0
  9. package/src/dashboard/templates/adaptive 2.html +118 -0
  10. package/src/dashboard/templates/artifacts 2.html +133 -0
  11. package/src/dashboard/templates/backups 2.html +136 -0
  12. package/src/dashboard/templates/base 2.html +417 -0
  13. package/src/dashboard/templates/calendar 2.html +591 -0
  14. package/src/dashboard/templates/chat 2.html +356 -0
  15. package/src/dashboard/templates/claims 2.html +259 -0
  16. package/src/dashboard/templates/cortex 2.html +321 -0
  17. package/src/dashboard/templates/credentials 2.html +128 -0
  18. package/src/dashboard/templates/crons 2.html +370 -0
  19. package/src/dashboard/templates/dashboard 2.html +494 -0
  20. package/src/dashboard/templates/dreams 2.html +252 -0
  21. package/src/dashboard/templates/email 2.html +160 -0
  22. package/src/dashboard/templates/evolution 2.html +189 -0
  23. package/src/dashboard/templates/feed 2.html +249 -0
  24. package/src/dashboard/templates/followup_health 2.html +170 -0
  25. package/src/dashboard/templates/graph 2.html +201 -0
  26. package/src/dashboard/templates/guard 2.html +259 -0
  27. package/src/dashboard/templates/inbox 2.html +251 -0
  28. package/src/dashboard/templates/memory 2.html +420 -0
  29. package/src/dashboard/templates/operations 2.html +608 -0
  30. package/src/dashboard/templates/plugins 2.html +185 -0
  31. package/src/dashboard/templates/protocol 2.html +199 -0
  32. package/src/dashboard/templates/rules 2.html +246 -0
  33. package/src/dashboard/templates/sentiment 2.html +247 -0
  34. package/src/dashboard/templates/sessions 2.html +218 -0
  35. package/src/dashboard/templates/skills 2.html +329 -0
  36. package/src/dashboard/templates/somatic 2.html +73 -0
  37. package/src/dashboard/templates/triggers 2.html +133 -0
  38. package/src/dashboard/templates/trust 2.html +360 -0
  39. package/src/db/__init__ 2.py +259 -0
  40. package/src/db/_core 2.py +437 -0
  41. package/src/db/_credentials 2.py +124 -0
  42. package/src/db/_entities.py +1 -1
  43. package/src/db/_episodic 2.py +762 -0
  44. package/src/db/_evolution 2.py +54 -0
  45. package/src/db/_fts 2.py +406 -0
  46. package/src/db/_goal_profiles 2.py +376 -0
  47. package/src/db/_hot_context 2.py +660 -0
  48. package/src/db/_outcomes 2.py +800 -0
  49. package/src/db/_personal_scripts 2.py +582 -0
  50. package/src/db/_sessions 2.py +330 -0
  51. package/src/db/_tasks 2.py +91 -0
  52. package/src/db/_watchers 2.py +173 -0
  53. package/src/doctor/formatters 2.py +52 -0
  54. package/src/doctor/models 2.py +69 -0
  55. package/src/doctor/planes 2.py +87 -0
  56. package/src/doctor/providers/__init__ 2.py +1 -0
  57. package/src/doctor/providers/deep 2.py +367 -0
  58. package/src/evolution_cycle 2.py +519 -0
  59. package/src/hooks/auto_capture 2.py +208 -0
  60. package/src/hooks/caffeinate-guard 2.sh +8 -0
  61. package/src/hooks/capture-session 2.sh +21 -0
  62. package/src/hooks/capture-tool-logs 2.sh +158 -0
  63. package/src/hooks/daily-briefing-check 2.sh +33 -0
  64. package/src/hooks/heartbeat-enforcement 2.py +90 -0
  65. package/src/hooks/heartbeat-posttool 2.sh +18 -0
  66. package/src/hooks/inbox-hook 2.sh +76 -0
  67. package/src/hooks/post-compact 2.sh +152 -0
  68. package/src/hooks/pre-compact 2.sh +169 -0
  69. package/src/hooks/protocol-guardrail 2.sh +10 -0
  70. package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
  71. package/src/hooks/session-stop 2.sh +52 -0
  72. package/src/kg_populate 2.py +292 -0
  73. package/src/maintenance 2.py +53 -0
  74. package/src/memory_backends 2.py +71 -0
  75. package/src/migrate_embeddings 2.py +124 -0
  76. package/src/nexo_sdk 2.py +103 -0
  77. package/src/observability 2.py +199 -0
  78. package/src/plugin_loader 2.py +217 -0
  79. package/src/plugins/__init__ 2.py +0 -0
  80. package/src/plugins/agents.py +10 -3
  81. package/src/plugins/artifact_registry 2.py +450 -0
  82. package/src/plugins/backup 2.py +127 -0
  83. package/src/plugins/claims_tools 2.py +119 -0
  84. package/src/plugins/cognitive_memory 2.py +609 -0
  85. package/src/plugins/core_rules 2.py +252 -0
  86. package/src/plugins/cortex 2.py +1155 -0
  87. package/src/plugins/entities 2.py +67 -0
  88. package/src/plugins/episodic_memory 2.py +560 -0
  89. package/src/plugins/evolution 2.py +167 -0
  90. package/src/plugins/goal_engine 2.py +142 -0
  91. package/src/plugins/guard 2.py +862 -0
  92. package/src/plugins/impact 2.py +29 -0
  93. package/src/plugins/knowledge_graph_tools 2.py +137 -0
  94. package/src/plugins/media_memory_tools 2.py +98 -0
  95. package/src/plugins/memory_export 2.py +196 -0
  96. package/src/plugins/outcomes 2.py +130 -0
  97. package/src/plugins/personal_scripts 2.py +117 -0
  98. package/src/plugins/preferences 2.py +47 -0
  99. package/src/plugins/protocol 2.py +1449 -0
  100. package/src/plugins/schedule.py +2 -1
  101. package/src/plugins/simple_api 2.py +106 -0
  102. package/src/plugins/skills 2.py +341 -0
  103. package/src/plugins/state_watchers 2.py +79 -0
  104. package/src/plugins/update 2.py +986 -0
  105. package/src/plugins/user_state_tools 2.py +43 -0
  106. package/src/plugins/workflow 2.py +588 -0
  107. package/src/protocol_settings 2.py +59 -0
  108. package/src/public_contribution 2.py +466 -0
  109. package/src/public_evolution_queue 2.py +241 -0
  110. package/src/requirements 2.txt +14 -0
  111. package/src/requirements.txt +1 -1
  112. package/src/retroactive_learnings 2.py +373 -0
  113. package/src/rules/__init__ 2.py +0 -0
  114. package/src/rules/core-rules 2.json +331 -0
  115. package/src/rules/migrate 2.py +207 -0
  116. package/src/runtime_power 2.py +874 -0
  117. package/src/runtime_power.py +18 -1
  118. package/src/script_registry 2.py +1559 -0
  119. package/src/scripts/check-context 2.py +272 -0
  120. package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
  121. package/src/scripts/deep-sleep/collect 2.py +928 -0
  122. package/src/scripts/deep-sleep/extract 2.py +330 -0
  123. package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
  124. package/src/scripts/deep-sleep/synthesize 2.py +312 -0
  125. package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
  126. package/src/scripts/nexo-agent-run 2.py +75 -0
  127. package/src/scripts/nexo-auto-update 2.py +6 -0
  128. package/src/scripts/nexo-backup 2.sh +25 -0
  129. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  130. package/src/scripts/nexo-catchup 2.py +300 -0
  131. package/src/scripts/nexo-cognitive-decay 2.py +257 -0
  132. package/src/scripts/nexo-cortex-cycle 2.py +293 -0
  133. package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
  134. package/src/scripts/nexo-cron-wrapper.sh +7 -0
  135. package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
  136. package/src/scripts/nexo-dashboard 2.sh +29 -0
  137. package/src/scripts/nexo-deep-sleep 2.sh +86 -0
  138. package/src/scripts/nexo-evolution-run 2.py +1664 -0
  139. package/src/scripts/nexo-followup-hygiene 2.py +139 -0
  140. package/src/scripts/nexo-hook-record 2.py +42 -0
  141. package/src/scripts/nexo-immune 2.py +936 -0
  142. package/src/scripts/nexo-impact-scorer 2.py +117 -0
  143. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  144. package/src/scripts/nexo-install 2.py +6 -0
  145. package/src/scripts/nexo-learning-housekeep 2.py +401 -0
  146. package/src/scripts/nexo-learning-validator 2.py +266 -0
  147. package/src/scripts/nexo-migrate 2.py +260 -0
  148. package/src/scripts/nexo-outcome-checker 2.py +127 -0
  149. package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
  150. package/src/scripts/nexo-pre-commit 2.py +120 -0
  151. package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
  152. package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
  153. package/src/scripts/nexo-reflection 2.py +256 -0
  154. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  155. package/src/scripts/nexo-sleep 2.py +631 -0
  156. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  157. package/src/scripts/nexo-sync-clients 2.py +16 -0
  158. package/src/scripts/nexo-synthesis 2.py +475 -0
  159. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  160. package/src/scripts/nexo-update 2.sh +306 -0
  161. package/src/scripts/nexo-watchdog 2.sh +1207 -0
  162. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  163. package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
  164. package/src/server 2.py +1296 -0
  165. package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
  166. package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
  167. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
  168. package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
  169. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
  170. package/src/skills/run-release-final-audit/guide 2.md +16 -0
  171. package/src/skills/run-release-final-audit/script 2.py +259 -0
  172. package/src/skills/run-release-final-audit/skill 2.json +77 -0
  173. package/src/skills/run-runtime-doctor/guide 2.md +12 -0
  174. package/src/skills/run-runtime-doctor/script 2.py +21 -0
  175. package/src/skills/run-runtime-doctor/skill 2.json +25 -0
  176. package/src/skills_runtime 2.py +932 -0
  177. package/src/state_watchers_runtime 2.py +475 -0
  178. package/src/storage_router 2.py +32 -0
  179. package/src/system_catalog 2.py +786 -0
  180. package/src/tools_coordination 2.py +103 -0
  181. package/src/tools_credentials 2.py +68 -0
  182. package/src/tools_drive 2.py +487 -0
  183. package/src/tools_hot_context 2.py +163 -0
  184. package/src/tools_learnings 2.py +612 -0
  185. package/src/tools_menu 2.py +229 -0
  186. package/src/tools_reminders 2.py +88 -0
  187. package/src/tools_reminders_crud 2.py +363 -0
  188. package/src/tools_sessions 2.py +1054 -0
  189. package/src/tools_system_catalog 2.py +19 -0
  190. package/src/tools_task_history 2.py +57 -0
  191. package/src/tools_transcripts 2.py +98 -0
  192. package/src/transcript_utils 2.py +412 -0
  193. package/src/user_context 2.py +46 -0
  194. package/src/user_data_portability 2.py +328 -0
  195. package/src/user_state_model 2.py +170 -0
  196. package/templates/CLAUDE.md 2.template +108 -0
  197. package/templates/CODEX.AGENTS.md 2.template +66 -0
  198. package/templates/launchagents/README 2.md +132 -0
  199. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
  200. package/templates/launchagents/com.nexo.auto-close-sessions.plist +1 -1
  201. package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
  202. package/templates/launchagents/com.nexo.catchup.plist +1 -1
  203. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
  204. package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
  205. package/templates/launchagents/com.nexo.dashboard.plist +1 -1
  206. package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
  207. package/templates/launchagents/com.nexo.deep-sleep.plist +1 -1
  208. package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
  209. package/templates/launchagents/com.nexo.evolution.plist +1 -1
  210. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
  211. package/templates/launchagents/com.nexo.followup-hygiene.plist +1 -1
  212. package/templates/launchagents/com.nexo.immune 2.plist +41 -0
  213. package/templates/launchagents/com.nexo.immune.plist +1 -1
  214. package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
  215. package/templates/launchagents/com.nexo.postmortem.plist +1 -1
  216. package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
  217. package/templates/launchagents/com.nexo.self-audit.plist +1 -1
  218. package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
  219. package/templates/launchagents/com.nexo.synthesis.plist +1 -1
  220. package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
  221. package/templates/launchagents/com.nexo.watchdog.plist +1 -1
  222. package/templates/nexo_helper 2.py +301 -0
  223. package/templates/openclaw 2.json +13 -0
  224. package/templates/plugin-template 2.py +40 -0
  225. package/templates/script-template 2.py +59 -0
  226. package/templates/script-template 2.sh +13 -0
  227. package/templates/script-template.py +5 -4
  228. package/templates/skill-script-template 2.py +48 -0
  229. package/templates/skill-script-template.py +2 -1
  230. 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