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,475 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
NEXO Synthesis Engine v2 — Daily intelligence brief.
|
|
4
|
+
|
|
5
|
+
Before: ~400 lines of Python concatenating SQL results into markdown sections.
|
|
6
|
+
Now: Collects raw data, passes to the configured automation backend which synthesizes
|
|
7
|
+
with real understanding of what matters for tomorrow.
|
|
8
|
+
|
|
9
|
+
Runs daily at 06:00 via LaunchAgent.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import fcntl
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sqlite3
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from datetime import datetime, date, timedelta
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from client_preferences import resolve_user_model as _resolve_user_model
|
|
24
|
+
_USER_MODEL = _resolve_user_model()
|
|
25
|
+
except Exception:
|
|
26
|
+
_USER_MODEL = ""
|
|
27
|
+
|
|
28
|
+
HOME = Path.home()
|
|
29
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
30
|
+
_script_dir = Path(__file__).resolve().parent
|
|
31
|
+
_repo_src = _script_dir.parent
|
|
32
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
|
|
33
|
+
if str(NEXO_CODE) not in sys.path:
|
|
34
|
+
sys.path.insert(0, str(NEXO_CODE))
|
|
35
|
+
|
|
36
|
+
from agent_runner import AutomationBackendUnavailableError, run_automation_prompt
|
|
37
|
+
|
|
38
|
+
CLAUDE_DIR = NEXO_HOME
|
|
39
|
+
COORD_DIR = CLAUDE_DIR / "coordination"
|
|
40
|
+
NEXO_DB = NEXO_HOME / "data" / "nexo.db"
|
|
41
|
+
OUTPUT_FILE = COORD_DIR / "daily-synthesis.md"
|
|
42
|
+
LAST_RUN_FILE = COORD_DIR / "synthesis-last-run"
|
|
43
|
+
LOCK_FILE = COORD_DIR / "synthesis.lock"
|
|
44
|
+
def _resolve_claude_cli() -> Path:
|
|
45
|
+
"""Find claude CLI: saved path > PATH > common locations."""
|
|
46
|
+
import shutil as _shutil
|
|
47
|
+
saved = NEXO_HOME / "config" / "claude-cli-path"
|
|
48
|
+
if saved.exists():
|
|
49
|
+
p = Path(saved.read_text().strip())
|
|
50
|
+
if p.exists():
|
|
51
|
+
return p
|
|
52
|
+
found = _shutil.which("claude")
|
|
53
|
+
if found:
|
|
54
|
+
return Path(found)
|
|
55
|
+
for candidate in [
|
|
56
|
+
HOME / ".local" / "bin" / "claude",
|
|
57
|
+
HOME / ".npm-global" / "bin" / "claude",
|
|
58
|
+
Path("/usr/local/bin/claude"),
|
|
59
|
+
]:
|
|
60
|
+
if candidate.exists():
|
|
61
|
+
return candidate
|
|
62
|
+
return HOME / ".local" / "bin" / "claude"
|
|
63
|
+
|
|
64
|
+
CLAUDE_CLI = _resolve_claude_cli()
|
|
65
|
+
|
|
66
|
+
TODAY = date.today()
|
|
67
|
+
TODAY_STR = TODAY.isoformat()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def log(msg: str):
|
|
71
|
+
ts = datetime.now().strftime("%H:%M:%S")
|
|
72
|
+
print(f"[{ts}] {msg}", flush=True)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def should_run() -> bool:
|
|
76
|
+
if LAST_RUN_FILE.exists():
|
|
77
|
+
return LAST_RUN_FILE.read_text().strip() != TODAY_STR
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def mark_done():
|
|
82
|
+
LAST_RUN_FILE.write_text(TODAY_STR)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def acquire_lock():
|
|
86
|
+
lock_fd = open(LOCK_FILE, "w")
|
|
87
|
+
try:
|
|
88
|
+
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
89
|
+
return lock_fd
|
|
90
|
+
except BlockingIOError:
|
|
91
|
+
log("Another instance running. Exiting.")
|
|
92
|
+
sys.exit(0)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def release_lock(lock_fd):
|
|
96
|
+
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
|
97
|
+
lock_fd.close()
|
|
98
|
+
LOCK_FILE.unlink(missing_ok=True)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def safe_query(sql: str, params=()) -> list:
|
|
102
|
+
if not NEXO_DB.exists():
|
|
103
|
+
return []
|
|
104
|
+
try:
|
|
105
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
106
|
+
conn.row_factory = sqlite3.Row
|
|
107
|
+
rows = [dict(r) for r in conn.execute(sql, params).fetchall()]
|
|
108
|
+
conn.close()
|
|
109
|
+
return rows
|
|
110
|
+
except Exception as e:
|
|
111
|
+
log(f"Query error: {e}")
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _table_columns(table_name: str) -> set[str]:
|
|
116
|
+
if not NEXO_DB.exists():
|
|
117
|
+
return set()
|
|
118
|
+
try:
|
|
119
|
+
conn = sqlite3.connect(str(NEXO_DB))
|
|
120
|
+
rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall()
|
|
121
|
+
conn.close()
|
|
122
|
+
return {str(row[1]) for row in rows}
|
|
123
|
+
except Exception:
|
|
124
|
+
return set()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_json_field(value):
|
|
128
|
+
if isinstance(value, str):
|
|
129
|
+
stripped = value.strip()
|
|
130
|
+
if not stripped:
|
|
131
|
+
return {}
|
|
132
|
+
try:
|
|
133
|
+
parsed = json.loads(stripped)
|
|
134
|
+
except json.JSONDecodeError:
|
|
135
|
+
return {}
|
|
136
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
137
|
+
return value if isinstance(value, dict) else {}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _impact_reasoning(row: dict) -> str:
|
|
141
|
+
factors = _parse_json_field(row.get("impact_factors"))
|
|
142
|
+
return str(factors.get("reasoning") or "").strip()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _load_json_summary(path: Path, *, actionable) -> tuple[dict | None, str | None]:
|
|
146
|
+
if not path.exists():
|
|
147
|
+
return None, None
|
|
148
|
+
try:
|
|
149
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
return None, str(exc)
|
|
152
|
+
if not isinstance(payload, dict):
|
|
153
|
+
return None, "summary payload is not a JSON object"
|
|
154
|
+
if not actionable(payload):
|
|
155
|
+
return None, None
|
|
156
|
+
return payload, None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _load_coordination_summary(filename: str, *, actionable) -> tuple[dict | None, str | None]:
|
|
160
|
+
return _load_json_summary(COORD_DIR / filename, actionable=actionable)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _update_summary_actionable(payload: dict) -> bool:
|
|
164
|
+
if any(payload.get(key) for key in ("error", "updated", "deferred_reason", "git_update", "npm_notice")):
|
|
165
|
+
return True
|
|
166
|
+
for action in payload.get("actions") or []:
|
|
167
|
+
if str(action).startswith("personal-schedules-"):
|
|
168
|
+
return True
|
|
169
|
+
for message in payload.get("client_bootstrap_updates") or []:
|
|
170
|
+
if "already current" not in str(message).lower():
|
|
171
|
+
return True
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def collect_data() -> dict:
|
|
176
|
+
"""Collect all raw data for synthesis."""
|
|
177
|
+
data = {"date": TODAY_STR}
|
|
178
|
+
|
|
179
|
+
# Today's learnings
|
|
180
|
+
data["learnings"] = safe_query(
|
|
181
|
+
"SELECT category, title, content, reasoning FROM learnings "
|
|
182
|
+
"WHERE date(created_at, 'unixepoch') = ? ORDER BY created_at DESC",
|
|
183
|
+
(TODAY_STR,)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Today's decisions
|
|
187
|
+
data["decisions"] = safe_query(
|
|
188
|
+
"SELECT domain, decision, alternatives, based_on, outcome FROM decisions "
|
|
189
|
+
"WHERE date(created_at) = ? ORDER BY created_at DESC",
|
|
190
|
+
(TODAY_STR,)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Today's changes
|
|
194
|
+
data["changes"] = safe_query(
|
|
195
|
+
"SELECT files, what_changed, why, affects, risks FROM change_log "
|
|
196
|
+
"WHERE date(created_at) = ? ORDER BY created_at DESC",
|
|
197
|
+
(TODAY_STR,)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Session diaries (summaries + mental_state)
|
|
201
|
+
data["diaries"] = safe_query(
|
|
202
|
+
"SELECT summary, self_critique, mental_state, user_signals FROM session_diary "
|
|
203
|
+
"WHERE date(created_at) = ? ORDER BY created_at DESC",
|
|
204
|
+
(TODAY_STR,)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Overdue reminders (schema: description, date, status uppercase)
|
|
208
|
+
data["overdue_reminders"] = safe_query(
|
|
209
|
+
"SELECT id, description, date FROM reminders "
|
|
210
|
+
"WHERE status='PENDING' AND date <= ? ORDER BY date",
|
|
211
|
+
(TODAY_STR,)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Pending followups (schema: description, date, status uppercase)
|
|
215
|
+
followup_columns = _table_columns("followups")
|
|
216
|
+
if "impact_score" in followup_columns:
|
|
217
|
+
impact_factors_sql = ", impact_factors" if "impact_factors" in followup_columns else ""
|
|
218
|
+
followup_select = (
|
|
219
|
+
"SELECT id, description, date, priority, impact_score"
|
|
220
|
+
f"{impact_factors_sql} FROM followups "
|
|
221
|
+
)
|
|
222
|
+
followup_order = (
|
|
223
|
+
"ORDER BY "
|
|
224
|
+
"CASE WHEN COALESCE(impact_score, 0) > 0 THEN 0 ELSE 1 END ASC, "
|
|
225
|
+
"COALESCE(impact_score, 0) DESC, "
|
|
226
|
+
"CASE WHEN date IS NULL OR date = '' THEN 1 ELSE 0 END ASC, "
|
|
227
|
+
"date ASC"
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
followup_select = "SELECT id, description, date FROM followups "
|
|
231
|
+
followup_order = "ORDER BY date"
|
|
232
|
+
data["pending_followups"] = safe_query(
|
|
233
|
+
f"{followup_select} WHERE status='PENDING' {followup_order}"
|
|
234
|
+
)
|
|
235
|
+
for row in data["pending_followups"]:
|
|
236
|
+
if "impact_factors" in row:
|
|
237
|
+
row["impact_factors"] = _parse_json_field(row.get("impact_factors"))
|
|
238
|
+
row["impact_reasoning"] = _impact_reasoning(row)
|
|
239
|
+
|
|
240
|
+
impact_summary_file = COORD_DIR / "impact-scorer-summary.json"
|
|
241
|
+
if impact_summary_file.exists():
|
|
242
|
+
try:
|
|
243
|
+
data["impact_queue_summary"] = json.loads(impact_summary_file.read_text(encoding="utf-8"))
|
|
244
|
+
except Exception as exc:
|
|
245
|
+
data["impact_queue_summary_error"] = str(exc)
|
|
246
|
+
|
|
247
|
+
followup_hygiene_summary, followup_hygiene_error = _load_coordination_summary(
|
|
248
|
+
"followup-hygiene-summary.json",
|
|
249
|
+
actionable=lambda payload: any(
|
|
250
|
+
int(payload.get(key, 0) or 0) > 0
|
|
251
|
+
for key in ("dirty_normalized", "stale_count", "orphan_count")
|
|
252
|
+
),
|
|
253
|
+
)
|
|
254
|
+
if followup_hygiene_summary is not None:
|
|
255
|
+
data["followup_hygiene_summary"] = followup_hygiene_summary
|
|
256
|
+
elif followup_hygiene_error:
|
|
257
|
+
data["followup_hygiene_summary_error"] = followup_hygiene_error
|
|
258
|
+
|
|
259
|
+
outcome_checker_summary, outcome_checker_error = _load_coordination_summary(
|
|
260
|
+
"outcome-checker-summary.json",
|
|
261
|
+
actionable=lambda payload: (
|
|
262
|
+
any(
|
|
263
|
+
int(payload.get(key, 0) or 0) > 0
|
|
264
|
+
for key in ("checked", "met", "missed", "pending", "errors")
|
|
265
|
+
)
|
|
266
|
+
or bool(payload.get("ids"))
|
|
267
|
+
or bool(((payload.get("auto_promoted_patterns") or {}).get("promoted") or []))
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
if outcome_checker_summary is not None:
|
|
271
|
+
data["outcome_checker_summary"] = outcome_checker_summary
|
|
272
|
+
elif outcome_checker_error:
|
|
273
|
+
data["outcome_checker_summary_error"] = outcome_checker_error
|
|
274
|
+
|
|
275
|
+
update_summary, update_summary_error = _load_json_summary(
|
|
276
|
+
NEXO_HOME / "logs" / "update-last-summary.json",
|
|
277
|
+
actionable=_update_summary_actionable,
|
|
278
|
+
)
|
|
279
|
+
if update_summary is not None:
|
|
280
|
+
data["update_summary"] = update_summary
|
|
281
|
+
elif update_summary_error:
|
|
282
|
+
data["update_summary_error"] = update_summary_error
|
|
283
|
+
|
|
284
|
+
# Guard stats
|
|
285
|
+
data["guard_stats"] = safe_query(
|
|
286
|
+
"SELECT category, COUNT(*) as cnt FROM learnings WHERE status='active' "
|
|
287
|
+
"GROUP BY category ORDER BY cnt DESC LIMIT 10"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Postmortem daily (if exists)
|
|
291
|
+
pm_file = COORD_DIR / "postmortem-daily.md"
|
|
292
|
+
if pm_file.exists():
|
|
293
|
+
data["postmortem_summary"] = pm_file.read_text()[:2000]
|
|
294
|
+
|
|
295
|
+
return data
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def synthesize(data: dict) -> bool:
|
|
299
|
+
"""CLI synthesizes the daily brief."""
|
|
300
|
+
|
|
301
|
+
data_json = json.dumps(data, ensure_ascii=False, indent=1)
|
|
302
|
+
if len(data_json) > 15000:
|
|
303
|
+
data_json = data_json[:15000] + "\n... (truncated)"
|
|
304
|
+
|
|
305
|
+
prompt = f"""FIRST: Call nexo_startup(task='daily synthesis') to register this session.
|
|
306
|
+
|
|
307
|
+
You are NEXO's synthesis engine. Write the daily intelligence brief for tomorrow's
|
|
308
|
+
startup. This file is read by NEXO at the beginning of each session to understand
|
|
309
|
+
what happened today and what to focus on tomorrow. Use nexo_learning_add and nexo_followup_create if you discover actionable items.
|
|
310
|
+
|
|
311
|
+
TODAY'S RAW DATA:
|
|
312
|
+
{data_json}
|
|
313
|
+
|
|
314
|
+
Write the synthesis to {OUTPUT_FILE} with this structure:
|
|
315
|
+
|
|
316
|
+
# NEXO Daily Synthesis — {TODAY_STR}
|
|
317
|
+
|
|
318
|
+
## Errors & Learnings
|
|
319
|
+
[New learnings from today — what went wrong, what was learned]
|
|
320
|
+
|
|
321
|
+
## Decisions Made
|
|
322
|
+
[Key decisions and their reasoning]
|
|
323
|
+
|
|
324
|
+
## Changes Deployed
|
|
325
|
+
[What was changed in production today]
|
|
326
|
+
|
|
327
|
+
## the user — Observations
|
|
328
|
+
[Patterns in the user's behavior: frustrations, pending decisions, ideas without
|
|
329
|
+
deadlines, topics he started but didn't close. This is NEXO's peripheral vision.]
|
|
330
|
+
|
|
331
|
+
## Weak Points (self-assessment)
|
|
332
|
+
[Where NEXO failed or could have done better today — from session diaries]
|
|
333
|
+
|
|
334
|
+
## Tomorrow's Context
|
|
335
|
+
[What the next session needs to know: pending followups, overdue reminders,
|
|
336
|
+
in-progress tasks, things to verify]
|
|
337
|
+
|
|
338
|
+
## Guard Status
|
|
339
|
+
[Areas with most learnings — where errors concentrate]
|
|
340
|
+
|
|
341
|
+
Be concise. Each section 3-8 bullet points max. Focus on what CHANGES BEHAVIOR,
|
|
342
|
+
not what merely happened. If a section has nothing, write "Nothing notable."
|
|
343
|
+
|
|
344
|
+
Execute without asking."""
|
|
345
|
+
|
|
346
|
+
log("Invoking automation backend for synthesis...")
|
|
347
|
+
try:
|
|
348
|
+
result = run_automation_prompt(
|
|
349
|
+
prompt,
|
|
350
|
+
model=_USER_MODEL or "opus",
|
|
351
|
+
timeout=21600,
|
|
352
|
+
output_format="text",
|
|
353
|
+
allowed_tools="Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*",
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
if result.returncode != 0:
|
|
357
|
+
log(f"CLI error ({result.returncode}): {(result.stderr or '')[:300]}")
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
log(f"Synthesis complete. Output: {len(result.stdout or '')} chars")
|
|
361
|
+
return True
|
|
362
|
+
|
|
363
|
+
except AutomationBackendUnavailableError as e:
|
|
364
|
+
log(f"Automation backend unavailable: {e}")
|
|
365
|
+
return False
|
|
366
|
+
except subprocess.TimeoutExpired:
|
|
367
|
+
log("CLI timed out (180s)")
|
|
368
|
+
return False
|
|
369
|
+
except Exception as e:
|
|
370
|
+
log(f"Exception: {e}")
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def fallback_synthesis(data: dict):
|
|
375
|
+
"""Write a basic synthesis from raw data when CLI is unavailable."""
|
|
376
|
+
log("Fallback: writing basic synthesis from raw data...")
|
|
377
|
+
lines = [f"# NEXO Daily Synthesis -- {TODAY_STR}", "",
|
|
378
|
+
"*(Generated by fallback -- CLI was unavailable)*", ""]
|
|
379
|
+
|
|
380
|
+
if data.get("learnings"):
|
|
381
|
+
lines.append("## Errors & Learnings")
|
|
382
|
+
for l in data["learnings"][:10]:
|
|
383
|
+
lines.append(f"- [{l.get('category', 'general')}] {l.get('title', 'untitled')}")
|
|
384
|
+
lines.append("")
|
|
385
|
+
|
|
386
|
+
if data.get("decisions"):
|
|
387
|
+
lines.append("## Decisions Made")
|
|
388
|
+
for d in data["decisions"][:10]:
|
|
389
|
+
lines.append(f"- [{d.get('domain', 'general')}] {d.get('decision', '')[:120]}")
|
|
390
|
+
lines.append("")
|
|
391
|
+
|
|
392
|
+
if data.get("changes"):
|
|
393
|
+
lines.append("## Changes Deployed")
|
|
394
|
+
for c in data["changes"][:10]:
|
|
395
|
+
lines.append(f"- {c.get('what_changed', '')[:120]}")
|
|
396
|
+
lines.append("")
|
|
397
|
+
|
|
398
|
+
if data.get("overdue_reminders"):
|
|
399
|
+
lines.append("## Overdue Reminders")
|
|
400
|
+
for r in data["overdue_reminders"][:10]:
|
|
401
|
+
lines.append(f"- #{r.get('id', '?')} {r.get('description', '')} (due {r.get('date', '?')})")
|
|
402
|
+
lines.append("")
|
|
403
|
+
|
|
404
|
+
if data.get("pending_followups"):
|
|
405
|
+
lines.append("## Pending Followups")
|
|
406
|
+
for f in data["pending_followups"][:10]:
|
|
407
|
+
impact = float(f.get("impact_score") or 0.0)
|
|
408
|
+
impact_tag = f" [impact {impact:.1f}]" if impact > 0 else ""
|
|
409
|
+
because = _impact_reasoning(f)
|
|
410
|
+
because_tag = f" — {because}" if because else ""
|
|
411
|
+
lines.append(
|
|
412
|
+
f"- #{f.get('id', '?')} {f.get('description', '')} "
|
|
413
|
+
f"(due {f.get('date', '?')}){impact_tag}{because_tag}"
|
|
414
|
+
)
|
|
415
|
+
lines.append("")
|
|
416
|
+
|
|
417
|
+
impact_summary = data.get("impact_queue_summary") or {}
|
|
418
|
+
if impact_summary.get("top_changes"):
|
|
419
|
+
lines.append("## Queue Changes By Impact")
|
|
420
|
+
for item in impact_summary.get("top_changes", [])[:5]:
|
|
421
|
+
delta = float(item.get("delta") or 0.0)
|
|
422
|
+
if abs(delta) < 1.0:
|
|
423
|
+
continue
|
|
424
|
+
direction = "+" if delta >= 0 else ""
|
|
425
|
+
lines.append(
|
|
426
|
+
f"- #{item.get('id', '?')} {direction}{delta:.1f} -> {float(item.get('impact_score') or 0.0):.1f}"
|
|
427
|
+
f" ({item.get('impact_reasoning') or 'score recalculated'})"
|
|
428
|
+
)
|
|
429
|
+
lines.append("")
|
|
430
|
+
|
|
431
|
+
OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
432
|
+
OUTPUT_FILE.write_text("\n".join(lines))
|
|
433
|
+
log(f"Fallback synthesis written to {OUTPUT_FILE}")
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def main():
|
|
437
|
+
if not should_run():
|
|
438
|
+
log(f"Already ran today ({TODAY_STR}). Skipping.")
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
lock_fd = acquire_lock()
|
|
442
|
+
try:
|
|
443
|
+
log(f"=== NEXO Synthesis v2 -- {TODAY_STR} ===")
|
|
444
|
+
|
|
445
|
+
data = collect_data()
|
|
446
|
+
log(f"Collected: {len(data.get('learnings', []))} learnings, "
|
|
447
|
+
f"{len(data.get('decisions', []))} decisions, "
|
|
448
|
+
f"{len(data.get('changes', []))} changes, "
|
|
449
|
+
f"{len(data.get('diaries', []))} diaries")
|
|
450
|
+
|
|
451
|
+
success = synthesize(data)
|
|
452
|
+
|
|
453
|
+
if success:
|
|
454
|
+
mark_done()
|
|
455
|
+
log("Synthesis v2 complete.")
|
|
456
|
+
else:
|
|
457
|
+
log("Synthesis CLI failed -- writing fallback synthesis.")
|
|
458
|
+
fallback_synthesis(data)
|
|
459
|
+
mark_done()
|
|
460
|
+
|
|
461
|
+
# Register for catch-up
|
|
462
|
+
try:
|
|
463
|
+
state_file = NEXO_HOME / "operations" / ".catchup-state.json"
|
|
464
|
+
st = json.loads(state_file.read_text()) if state_file.exists() else {}
|
|
465
|
+
st["synthesis"] = datetime.now().isoformat()
|
|
466
|
+
state_file.write_text(json.dumps(st, indent=2))
|
|
467
|
+
except Exception:
|
|
468
|
+
pass
|
|
469
|
+
|
|
470
|
+
finally:
|
|
471
|
+
release_lock(lock_fd)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
if __name__ == "__main__":
|
|
475
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# NEXO TCC Auto-Approve — grants macOS permissions to new Claude Code versions.
|
|
3
|
+
#
|
|
4
|
+
# macOS only. On Linux this is a no-op (Linux doesn't have TCC).
|
|
5
|
+
# Runs at load to approve any new Claude versions that appeared.
|
|
6
|
+
#
|
|
7
|
+
# What it does:
|
|
8
|
+
# 1. Scans ~/.local/share/claude/versions/ for Claude binaries
|
|
9
|
+
# 2. For each new version, grants TCC access to Documents, Desktop, Downloads, etc.
|
|
10
|
+
# 3. Also approves the Python binary used by NEXO's venv
|
|
11
|
+
# 4. Tracks which versions have been approved to avoid re-processing
|
|
12
|
+
#
|
|
13
|
+
# Why: Claude Code updates frequently. Each new binary needs macOS permission
|
|
14
|
+
# grants or the user gets popup dialogs interrupting their work.
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
# Linux: nothing to do
|
|
19
|
+
if [ "$(uname -s)" != "Darwin" ]; then
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
24
|
+
TCC_DB="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
|
|
25
|
+
VERSIONS_DIR="$HOME/.local/share/claude/versions"
|
|
26
|
+
MARKER_DIR="$NEXO_HOME/data/.tcc-approved"
|
|
27
|
+
LOG="$NEXO_HOME/logs/tcc-auto-approve.log"
|
|
28
|
+
|
|
29
|
+
mkdir -p "$MARKER_DIR" "$(dirname "$LOG")"
|
|
30
|
+
|
|
31
|
+
# TCC services Claude Code needs
|
|
32
|
+
SERVICES=(
|
|
33
|
+
kTCCServiceSystemPolicyDocumentsFolder
|
|
34
|
+
kTCCServiceSystemPolicyDesktopFolder
|
|
35
|
+
kTCCServiceSystemPolicyDownloadsFolder
|
|
36
|
+
kTCCServiceMediaLibrary
|
|
37
|
+
kTCCServiceSystemPolicyNetworkVolumes
|
|
38
|
+
kTCCServiceSystemPolicyAppData
|
|
39
|
+
kTCCServiceFileProviderDomain
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Approve Claude versions
|
|
43
|
+
if [ -d "$VERSIONS_DIR" ]; then
|
|
44
|
+
for bin_path in "$VERSIONS_DIR"/*; do
|
|
45
|
+
[ ! -e "$bin_path" ] && continue
|
|
46
|
+
version=$(basename "$bin_path")
|
|
47
|
+
marker="$MARKER_DIR/$version"
|
|
48
|
+
|
|
49
|
+
# Skip if already approved
|
|
50
|
+
[ -f "$marker" ] && continue
|
|
51
|
+
|
|
52
|
+
echo "$(date '+%Y-%m-%d %H:%M:%S') Approving Claude $version" >> "$LOG"
|
|
53
|
+
|
|
54
|
+
for svc in "${SERVICES[@]}"; do
|
|
55
|
+
sqlite3 "$TCC_DB" "
|
|
56
|
+
INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version)
|
|
57
|
+
VALUES ('$svc', '$bin_path', 1, 2, 4, 1);
|
|
58
|
+
" 2>/dev/null
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
touch "$marker"
|
|
62
|
+
echo "$(date '+%Y-%m-%d %H:%M:%S') Done: Claude $version — ${#SERVICES[@]} services approved" >> "$LOG"
|
|
63
|
+
done
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Also approve Python from NEXO's venv (if it exists)
|
|
67
|
+
NEXO_CODE="${NEXO_CODE:-}"
|
|
68
|
+
if [ -n "$NEXO_CODE" ]; then
|
|
69
|
+
PYTHON_BIN="$(dirname "$NEXO_CODE")/.venv/bin/python"
|
|
70
|
+
if [ -e "$PYTHON_BIN" ]; then
|
|
71
|
+
PYTHON_REAL=$(readlink -f "$PYTHON_BIN" 2>/dev/null || echo "$PYTHON_BIN")
|
|
72
|
+
for svc in "${SERVICES[@]}"; do
|
|
73
|
+
sqlite3 "$TCC_DB" "
|
|
74
|
+
INSERT OR REPLACE INTO access (service, client, client_type, auth_value, auth_reason, auth_version)
|
|
75
|
+
VALUES ('$svc', '$PYTHON_REAL', 1, 2, 4, 1);
|
|
76
|
+
" 2>/dev/null
|
|
77
|
+
done
|
|
78
|
+
fi
|
|
79
|
+
fi
|