nexo-brain 1.7.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -60
- package/bin/nexo-brain.js +680 -381
- package/package.json +18 -3
- package/scripts/migrate-to-unified.sh +813 -0
- package/scripts/migrate-v1.7-to-v1.8.py +214 -0
- package/scripts/pre-commit-check.sh +1 -1
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions.py +1 -1
- package/src/auto_update.py +634 -0
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core.py +7 -3
- package/src/cognitive/_decay.py +1 -1
- package/src/cognitive/_search.py +1 -0
- package/src/cognitive/_trust.py +3 -3
- package/src/crons/manifest.json +106 -0
- package/src/crons/sync.py +217 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app.py +24 -4
- package/src/dashboard/templates/dashboard.html +3 -2
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core.py +5 -1
- package/src/db/_episodic.py +2 -4
- package/src/db/_reminders.py +45 -6
- package/src/db/_schema.py +31 -0
- package/src/evolution_cycle.py +33 -11
- package/src/hooks/auto_capture.py +1 -1
- package/src/hooks/capture-tool-logs.sh +76 -0
- package/src/hooks/inbox-hook.sh +2 -1
- package/src/hooks/post-compact.sh +2 -1
- package/src/hooks/pre-compact.sh +104 -2
- package/src/hooks/session-start.sh +6 -2
- package/src/hooks/session-stop.sh +4 -2
- package/src/kg_populate.py +4 -1
- package/src/migrate_embeddings.py +4 -1
- package/src/plugin_loader.py +100 -34
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/agents.py +2 -2
- package/src/plugins/backup.py +5 -4
- package/src/plugins/core_rules.py +37 -16
- package/src/plugins/episodic_memory.py +14 -5
- package/src/plugins/evolution.py +6 -2
- package/src/plugins/guard.py +20 -11
- package/src/plugins/update.py +256 -0
- package/src/requirements.txt +12 -0
- package/src/scripts/check-context.py +8 -3
- package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/apply_findings.py +514 -169
- package/src/scripts/deep-sleep/collect.py +480 -0
- package/src/scripts/deep-sleep/extract-prompt.md +233 -0
- package/src/scripts/deep-sleep/extract.py +249 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +168 -0
- package/src/scripts/deep-sleep/synthesize.py +191 -0
- package/src/scripts/nexo-auto-update.py +4 -211
- package/src/scripts/nexo-backup.sh +5 -13
- package/src/scripts/nexo-brain-activation.sh +26 -26
- package/src/scripts/nexo-catchup.py +36 -25
- package/src/scripts/nexo-cognitive-decay.py +7 -3
- package/src/scripts/nexo-daily-self-audit.py +44 -15
- package/src/scripts/nexo-deep-sleep.sh +31 -16
- package/src/scripts/nexo-evolution-run.py +21 -11
- package/src/scripts/nexo-followup-hygiene.py +6 -4
- package/src/scripts/nexo-github-monitor.py +12 -8
- package/src/scripts/nexo-immune.py +6 -4
- package/src/scripts/nexo-inbox-hook.sh +2 -1
- package/src/scripts/nexo-install.py +4 -225
- package/src/scripts/nexo-learning-housekeep.py +7 -3
- package/src/scripts/nexo-learning-validator.py +1 -22
- package/src/scripts/nexo-migrate.py +9 -3
- package/src/scripts/nexo-postmortem-consolidator.py +17 -10
- package/src/scripts/nexo-pre-commit.py +3 -1
- package/src/scripts/nexo-prevent-sleep.sh +29 -0
- package/src/scripts/nexo-proactive-dashboard.py +5 -4
- package/src/scripts/nexo-runtime-preflight.py +59 -55
- package/src/scripts/nexo-send-email.py +1 -1
- package/src/scripts/nexo-send-reply.py +3 -1
- package/src/scripts/nexo-sleep.py +13 -9
- package/src/scripts/nexo-snapshot-restore.sh +2 -1
- package/src/scripts/nexo-synthesis.py +11 -7
- package/src/scripts/nexo-tcc-approve.sh +79 -0
- package/src/scripts/nexo-update.sh +161 -0
- package/src/scripts/nexo-watchdog-smoke.py +18 -13
- package/src/scripts/nexo-watchdog.sh +22 -13
- package/src/server.py +77 -28
- package/src/storage_router.py +6 -2
- package/src/tools_learnings.py +6 -6
- package/src/tools_menu.py +1 -1
- package/src/tools_reminders_crud.py +10 -8
- package/src/tools_sessions.py +76 -4
- package/templates/CLAUDE.md.template +14 -80
- package/templates/launchagents/README.md +7 -7
- package/templates/launchagents/com.nexo.auto-close-sessions.plist +5 -1
- package/templates/launchagents/com.nexo.catchup.plist +4 -0
- package/templates/launchagents/com.nexo.cognitive-decay.plist +7 -0
- package/templates/launchagents/com.nexo.dashboard.plist +5 -1
- package/templates/launchagents/com.nexo.deep-sleep.plist +4 -0
- package/templates/launchagents/com.nexo.evolution.plist +4 -0
- package/templates/launchagents/com.nexo.followup-hygiene.plist +4 -0
- package/templates/launchagents/com.nexo.github-monitor.plist +3 -1
- package/templates/launchagents/com.nexo.immune.plist +4 -0
- package/templates/launchagents/com.nexo.postmortem.plist +4 -0
- package/templates/launchagents/com.nexo.self-audit.plist +4 -0
- package/templates/launchagents/com.nexo.synthesis.plist +4 -0
- package/templates/launchagents/com.nexo.watchdog.plist +4 -0
- package/templates/openclaw.json +1 -1
- package/tests/conftest.py +2 -2
- package/tests/test_cognitive.py +7 -6
- package/tests/test_migrations.py +26 -0
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
- package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
- package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/server.cpython-314.pyc +0 -0
- package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
- package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
- package/src/scripts/deep-sleep/analyze_session.py +0 -217
- package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
- package/src/scripts/deep-sleep/prompt.md +0 -109
- package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Deep Sleep v2 -- Phase 3: Synthesize extractions into actionable findings.
|
|
4
|
+
|
|
5
|
+
One Claude call that reads all per-session extractions and produces a
|
|
6
|
+
unified synthesis with cross-session patterns, morning agenda, context
|
|
7
|
+
packets, and deduplicated actions.
|
|
8
|
+
|
|
9
|
+
Environment variables:
|
|
10
|
+
NEXO_HOME -- root of the NEXO installation (default: ~/.nexo)
|
|
11
|
+
"""
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
21
|
+
DEEP_SLEEP_DIR = NEXO_HOME / "operations" / "deep-sleep"
|
|
22
|
+
PROMPT_FILE = Path(__file__).parent / "synthesize-prompt.md"
|
|
23
|
+
|
|
24
|
+
CLAUDE_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_claude_cli() -> str:
|
|
28
|
+
"""Find the Claude CLI binary."""
|
|
29
|
+
candidates = [
|
|
30
|
+
Path.home() / ".local" / "bin" / "claude",
|
|
31
|
+
Path("/usr/local/bin/claude"),
|
|
32
|
+
]
|
|
33
|
+
for c in candidates:
|
|
34
|
+
if c.exists():
|
|
35
|
+
return str(c)
|
|
36
|
+
which = shutil.which("claude")
|
|
37
|
+
if which:
|
|
38
|
+
return which
|
|
39
|
+
return "claude"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def extract_json_from_response(text: str) -> dict | None:
|
|
43
|
+
"""Parse JSON from Claude's response, handling markdown fences."""
|
|
44
|
+
text = text.strip()
|
|
45
|
+
|
|
46
|
+
if text.startswith("```"):
|
|
47
|
+
lines = text.split("\n")
|
|
48
|
+
end = len(lines)
|
|
49
|
+
for i in range(len(lines) - 1, 0, -1):
|
|
50
|
+
if lines[i].strip() == "```":
|
|
51
|
+
end = i
|
|
52
|
+
break
|
|
53
|
+
text = "\n".join(lines[1:end]).strip()
|
|
54
|
+
|
|
55
|
+
brace_start = text.find("{")
|
|
56
|
+
if brace_start < 0:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
depth = 0
|
|
60
|
+
for i in range(brace_start, len(text)):
|
|
61
|
+
if text[i] == "{":
|
|
62
|
+
depth += 1
|
|
63
|
+
elif text[i] == "}":
|
|
64
|
+
depth -= 1
|
|
65
|
+
if depth == 0:
|
|
66
|
+
try:
|
|
67
|
+
return json.loads(text[brace_start:i + 1])
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
break
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
target_date = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
|
|
75
|
+
|
|
76
|
+
extractions_file = DEEP_SLEEP_DIR / f"{target_date}-extractions.json"
|
|
77
|
+
context_file = DEEP_SLEEP_DIR / f"{target_date}-context.txt"
|
|
78
|
+
|
|
79
|
+
if not extractions_file.exists():
|
|
80
|
+
print(f"[synthesize] No extractions file for {target_date}. Run extract.py first.")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
# Check if there are any findings worth synthesizing
|
|
84
|
+
with open(extractions_file) as f:
|
|
85
|
+
extractions = json.load(f)
|
|
86
|
+
|
|
87
|
+
total_findings = extractions.get("total_findings", 0)
|
|
88
|
+
if total_findings == 0:
|
|
89
|
+
print(f"[synthesize] No findings to synthesize for {target_date}.")
|
|
90
|
+
# Write minimal synthesis
|
|
91
|
+
output = {
|
|
92
|
+
"date": target_date,
|
|
93
|
+
"sessions_analyzed": extractions.get("sessions_analyzed", 0),
|
|
94
|
+
"cross_session_patterns": [],
|
|
95
|
+
"morning_agenda": [],
|
|
96
|
+
"context_packets": [],
|
|
97
|
+
"actions": [],
|
|
98
|
+
"summary": f"No significant findings for {target_date}."
|
|
99
|
+
}
|
|
100
|
+
output_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
|
|
101
|
+
with open(output_file, "w") as f:
|
|
102
|
+
json.dump(output, f, indent=2, ensure_ascii=False)
|
|
103
|
+
print(f"[synthesize] Output: {output_file}")
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
# Build prompt
|
|
107
|
+
prompt_template = PROMPT_FILE.read_text()
|
|
108
|
+
prompt = prompt_template.replace("{{EXTRACTIONS_FILE}}", str(extractions_file))
|
|
109
|
+
prompt = prompt.replace("{{CONTEXT_FILE}}", str(context_file))
|
|
110
|
+
|
|
111
|
+
claude_bin = find_claude_cli()
|
|
112
|
+
print(f"[synthesize] Phase 3: Synthesizing {total_findings} findings from {target_date}")
|
|
113
|
+
print(f"[synthesize] Claude CLI: {claude_bin}")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
env = os.environ.copy()
|
|
117
|
+
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
118
|
+
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
[
|
|
121
|
+
claude_bin,
|
|
122
|
+
"-p", prompt,
|
|
123
|
+
"--model", "opus",
|
|
124
|
+
"--output-format", "text",
|
|
125
|
+
"--allowedTools",
|
|
126
|
+
"Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__nexo_startup,mcp__nexo__nexo_learning_search,mcp__nexo__nexo_recall,mcp__nexo__nexo_reminders"
|
|
127
|
+
],
|
|
128
|
+
capture_output=True,
|
|
129
|
+
text=True,
|
|
130
|
+
timeout=CLAUDE_TIMEOUT,
|
|
131
|
+
env=env
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if result.returncode != 0:
|
|
135
|
+
print(f"[synthesize] Claude CLI error (exit {result.returncode}): {result.stderr[:300]}", file=sys.stderr)
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
|
|
138
|
+
# Filter hook contamination
|
|
139
|
+
output_text = "\n".join(
|
|
140
|
+
l for l in result.stdout.strip().splitlines()
|
|
141
|
+
if not l.strip().startswith("Post-mortem")
|
|
142
|
+
)
|
|
143
|
+
parsed = extract_json_from_response(output_text)
|
|
144
|
+
|
|
145
|
+
# Fallback: Opus might have written the file directly via Write tool
|
|
146
|
+
if not parsed:
|
|
147
|
+
for candidate in [
|
|
148
|
+
DEEP_SLEEP_DIR / f"{target_date}-analysis.json",
|
|
149
|
+
DEEP_SLEEP_DIR / f"{target_date}-synthesis.json",
|
|
150
|
+
]:
|
|
151
|
+
if candidate.exists() and candidate.stat().st_size > 100:
|
|
152
|
+
try:
|
|
153
|
+
parsed = json.load(open(candidate))
|
|
154
|
+
print(f"[synthesize] Opus wrote file directly: {candidate}")
|
|
155
|
+
break
|
|
156
|
+
except Exception:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
if not parsed:
|
|
160
|
+
debug_file = DEEP_SLEEP_DIR / f"debug-synthesize-{target_date}.txt"
|
|
161
|
+
debug_file.write_text(result.stdout[:10000])
|
|
162
|
+
print(f"[synthesize] Failed to parse JSON. Raw output saved to {debug_file}", file=sys.stderr)
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
165
|
+
# Write synthesis output
|
|
166
|
+
output_file = DEEP_SLEEP_DIR / f"{target_date}-synthesis.json"
|
|
167
|
+
with open(output_file, "w") as f:
|
|
168
|
+
json.dump(parsed, f, indent=2, ensure_ascii=False)
|
|
169
|
+
|
|
170
|
+
n_actions = len(parsed.get("actions", []))
|
|
171
|
+
n_patterns = len(parsed.get("cross_session_patterns", []))
|
|
172
|
+
n_agenda = len(parsed.get("morning_agenda", []))
|
|
173
|
+
n_packets = len(parsed.get("context_packets", []))
|
|
174
|
+
|
|
175
|
+
print(f"[synthesize] Done.")
|
|
176
|
+
print(f" Actions: {n_actions}")
|
|
177
|
+
print(f" Cross-session patterns: {n_patterns}")
|
|
178
|
+
print(f" Morning agenda items: {n_agenda}")
|
|
179
|
+
print(f" Context packets: {n_packets}")
|
|
180
|
+
print(f"[synthesize] Output: {output_file}")
|
|
181
|
+
|
|
182
|
+
except subprocess.TimeoutExpired:
|
|
183
|
+
print(f"[synthesize] Claude CLI timeout ({CLAUDE_TIMEOUT}s)", file=sys.stderr)
|
|
184
|
+
sys.exit(1)
|
|
185
|
+
except FileNotFoundError:
|
|
186
|
+
print(f"[synthesize] Claude CLI not found at: {claude_bin}", file=sys.stderr)
|
|
187
|
+
sys.exit(1)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
|
@@ -1,213 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
NEXO Auto-Update — checks for new versions and applies updates.
|
|
4
|
-
|
|
5
|
-
Runs at boot via the catch-up system or manually.
|
|
6
|
-
Compares local version with the latest GitHub release.
|
|
7
|
-
If a new version is available, downloads and applies the update.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
import shutil
|
|
13
|
-
import subprocess
|
|
2
|
+
"""DEPRECATED: Updates are handled automatically by NEXO on startup."""
|
|
14
3
|
import sys
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
19
|
-
VERSION_FILE = NEXO_HOME / "version.json"
|
|
20
|
-
LOG_FILE = NEXO_HOME / "logs" / "auto-update.log"
|
|
21
|
-
REPO = "wazionapps/nexo"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def log(msg: str):
|
|
25
|
-
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
26
|
-
line = f"[{ts}] {msg}"
|
|
27
|
-
print(line, flush=True)
|
|
28
|
-
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
-
with open(LOG_FILE, "a") as f:
|
|
30
|
-
f.write(line + "\n")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def get_local_version() -> str:
|
|
34
|
-
"""Read locally installed version."""
|
|
35
|
-
if VERSION_FILE.exists():
|
|
36
|
-
try:
|
|
37
|
-
return json.loads(VERSION_FILE.read_text()).get("version", "0.0.0")
|
|
38
|
-
except Exception:
|
|
39
|
-
pass
|
|
40
|
-
return "0.0.0"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def get_remote_version() -> dict | None:
|
|
44
|
-
"""Check latest release on GitHub."""
|
|
45
|
-
try:
|
|
46
|
-
result = subprocess.run(
|
|
47
|
-
["gh", "api", f"repos/{REPO}/releases/latest"],
|
|
48
|
-
capture_output=True, text=True, timeout=15
|
|
49
|
-
)
|
|
50
|
-
if result.returncode == 0:
|
|
51
|
-
data = json.loads(result.stdout)
|
|
52
|
-
return {
|
|
53
|
-
"version": data.get("tag_name", "").lstrip("v"),
|
|
54
|
-
"tarball_url": data.get("tarball_url", ""),
|
|
55
|
-
"published_at": data.get("published_at", ""),
|
|
56
|
-
"body": data.get("body", "")[:500],
|
|
57
|
-
}
|
|
58
|
-
except Exception:
|
|
59
|
-
pass
|
|
60
|
-
return None
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def version_compare(local: str, remote: str) -> int:
|
|
64
|
-
"""Compare semver strings. Returns -1 (local older), 0 (same), 1 (local newer)."""
|
|
65
|
-
def parse(v):
|
|
66
|
-
parts = v.split(".")
|
|
67
|
-
return tuple(int(p) for p in parts if p.isdigit())
|
|
68
|
-
|
|
69
|
-
l, r = parse(local), parse(remote)
|
|
70
|
-
if l < r:
|
|
71
|
-
return -1
|
|
72
|
-
elif l > r:
|
|
73
|
-
return 1
|
|
74
|
-
return 0
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def apply_update(tarball_url: str, new_version: str) -> bool:
|
|
78
|
-
"""Download and apply update from GitHub release tarball."""
|
|
79
|
-
import tempfile
|
|
80
|
-
|
|
81
|
-
log(f"Downloading update v{new_version}...")
|
|
82
|
-
tmp_dir = Path(tempfile.mkdtemp(prefix="nexo-update-"))
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
# Download tarball
|
|
86
|
-
tarball = tmp_dir / "release.tar.gz"
|
|
87
|
-
result = subprocess.run(
|
|
88
|
-
["curl", "-sL", "-o", str(tarball), tarball_url],
|
|
89
|
-
capture_output=True, timeout=60
|
|
90
|
-
)
|
|
91
|
-
if result.returncode != 0:
|
|
92
|
-
log(f"Download failed: {result.stderr}")
|
|
93
|
-
return False
|
|
94
|
-
|
|
95
|
-
# Extract
|
|
96
|
-
subprocess.run(
|
|
97
|
-
["tar", "xzf", str(tarball), "-C", str(tmp_dir)],
|
|
98
|
-
capture_output=True, timeout=30
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
# Find extracted directory (GitHub tarballs have a top-level dir)
|
|
102
|
-
extracted = [d for d in tmp_dir.iterdir() if d.is_dir()]
|
|
103
|
-
if not extracted:
|
|
104
|
-
log("No directory found in tarball")
|
|
105
|
-
return False
|
|
106
|
-
|
|
107
|
-
src_dir = extracted[0] / "src"
|
|
108
|
-
if not src_dir.exists():
|
|
109
|
-
log("No src/ directory in release")
|
|
110
|
-
return False
|
|
111
|
-
|
|
112
|
-
# Backup current files
|
|
113
|
-
backup_dir = NEXO_HOME / "backups" / f"pre-update-{new_version}"
|
|
114
|
-
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
115
|
-
|
|
116
|
-
files_updated = 0
|
|
117
|
-
# Update core Python files
|
|
118
|
-
for py_file in src_dir.glob("*.py"):
|
|
119
|
-
dest = NEXO_HOME / py_file.name
|
|
120
|
-
if dest.exists():
|
|
121
|
-
shutil.copy2(dest, backup_dir / py_file.name)
|
|
122
|
-
shutil.copy2(py_file, dest)
|
|
123
|
-
files_updated += 1
|
|
124
|
-
|
|
125
|
-
# Update plugins
|
|
126
|
-
plugins_src = src_dir / "plugins"
|
|
127
|
-
if plugins_src.exists():
|
|
128
|
-
plugins_dest = NEXO_HOME / "plugins"
|
|
129
|
-
plugins_dest.mkdir(exist_ok=True)
|
|
130
|
-
for py_file in plugins_src.glob("*.py"):
|
|
131
|
-
dest = plugins_dest / py_file.name
|
|
132
|
-
if dest.exists():
|
|
133
|
-
shutil.copy2(dest, backup_dir / f"plugin_{py_file.name}")
|
|
134
|
-
shutil.copy2(py_file, dest)
|
|
135
|
-
files_updated += 1
|
|
136
|
-
|
|
137
|
-
# Update scripts
|
|
138
|
-
scripts_src = src_dir / "scripts"
|
|
139
|
-
if scripts_src.exists():
|
|
140
|
-
scripts_dest = NEXO_HOME / "scripts"
|
|
141
|
-
scripts_dest.mkdir(exist_ok=True)
|
|
142
|
-
for py_file in scripts_src.glob("*.py"):
|
|
143
|
-
dest = scripts_dest / py_file.name
|
|
144
|
-
if dest.exists():
|
|
145
|
-
shutil.copy2(dest, backup_dir / f"script_{py_file.name}")
|
|
146
|
-
shutil.copy2(py_file, dest)
|
|
147
|
-
files_updated += 1
|
|
148
|
-
|
|
149
|
-
# Update hooks
|
|
150
|
-
hooks_src = src_dir / "hooks"
|
|
151
|
-
if hooks_src.exists():
|
|
152
|
-
hooks_dest = NEXO_HOME / "hooks"
|
|
153
|
-
hooks_dest.mkdir(exist_ok=True)
|
|
154
|
-
for sh_file in hooks_src.glob("*.sh"):
|
|
155
|
-
dest = hooks_dest / sh_file.name
|
|
156
|
-
shutil.copy2(sh_file, dest)
|
|
157
|
-
os.chmod(dest, 0o755)
|
|
158
|
-
files_updated += 1
|
|
159
|
-
|
|
160
|
-
# Save new version
|
|
161
|
-
VERSION_FILE.write_text(json.dumps({
|
|
162
|
-
"version": new_version,
|
|
163
|
-
"updated_at": datetime.now().isoformat(),
|
|
164
|
-
"files_updated": files_updated,
|
|
165
|
-
"backup": str(backup_dir),
|
|
166
|
-
}, indent=2))
|
|
167
|
-
|
|
168
|
-
log(f"Update applied: {files_updated} files updated. Backup at {backup_dir}")
|
|
169
|
-
return True
|
|
170
|
-
|
|
171
|
-
except Exception as e:
|
|
172
|
-
log(f"Update failed: {e}")
|
|
173
|
-
return False
|
|
174
|
-
finally:
|
|
175
|
-
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def main():
|
|
179
|
-
log("=== NEXO Auto-Update check ===")
|
|
180
|
-
|
|
181
|
-
local_ver = get_local_version()
|
|
182
|
-
log(f"Local version: {local_ver}")
|
|
183
|
-
|
|
184
|
-
remote = get_remote_version()
|
|
185
|
-
if not remote:
|
|
186
|
-
log("Could not check remote version (no network or no releases)")
|
|
187
|
-
return
|
|
188
|
-
|
|
189
|
-
remote_ver = remote["version"]
|
|
190
|
-
log(f"Remote version: {remote_ver}")
|
|
191
|
-
|
|
192
|
-
cmp = version_compare(local_ver, remote_ver)
|
|
193
|
-
if cmp >= 0:
|
|
194
|
-
log("Already up to date.")
|
|
195
|
-
return
|
|
196
|
-
|
|
197
|
-
log(f"Update available: {local_ver} → {remote_ver}")
|
|
198
|
-
log(f"Release notes: {remote['body'][:200]}")
|
|
199
|
-
|
|
200
|
-
if remote["tarball_url"]:
|
|
201
|
-
success = apply_update(remote["tarball_url"], remote_ver)
|
|
202
|
-
if success:
|
|
203
|
-
log(f"Successfully updated to v{remote_ver}")
|
|
204
|
-
else:
|
|
205
|
-
log("Update failed — will retry next boot")
|
|
206
|
-
else:
|
|
207
|
-
log("No tarball URL in release — manual update needed")
|
|
208
|
-
|
|
209
|
-
log("=== Done ===")
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if __name__ == "__main__":
|
|
213
|
-
main()
|
|
4
|
+
print("This script is deprecated. NEXO auto-updates on startup.")
|
|
5
|
+
print("To update manually, use the nexo_update MCP tool.")
|
|
6
|
+
sys.exit(0)
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# NEXO DB hourly backup
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
NEXO_DIR="${NEXO_DIR:-$(dirname "$(dirname "$(realpath "$0")")")}"
|
|
8
|
-
BACKUP_DIR="$NEXO_DIR/backups"
|
|
2
|
+
# NEXO DB hourly backup — crontab: 0 * * * * $NEXO_HOME/scripts/nexo-backup.sh
|
|
3
|
+
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
4
|
+
NEXO_DIR="$NEXO_HOME"
|
|
5
|
+
BACKUP_DIR="$NEXO_HOME/backups"
|
|
9
6
|
WEEKLY_DIR="$BACKUP_DIR/weekly"
|
|
10
|
-
DB="$
|
|
7
|
+
DB="$NEXO_HOME/data/nexo.db"
|
|
11
8
|
RETENTION_HOURS=48
|
|
12
9
|
|
|
13
|
-
if [ ! -f "$DB" ]; then
|
|
14
|
-
echo "ERROR: nexo.db not found at $DB" >&2
|
|
15
|
-
exit 1
|
|
16
|
-
fi
|
|
17
|
-
|
|
18
10
|
mkdir -p "$BACKUP_DIR" "$WEEKLY_DIR"
|
|
19
11
|
|
|
20
12
|
# Hourly backup
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# nexo-brain-activation.sh — NF24:
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# nexo-brain-activation.sh — NF24: Spontaneous Activation
|
|
3
|
+
# Reads user_model.json, detects significant shifts, and generates insights
|
|
4
|
+
# for NEXO startup. If nothing relevant: exit 0 with no output.
|
|
5
5
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
@@ -9,12 +9,12 @@ BRAIN_DIR="~/.nexo/brain"
|
|
|
9
9
|
MODEL_FILE="$BRAIN_DIR/user_model.json"
|
|
10
10
|
SUMMARIES_DIR="$BRAIN_DIR/daily_summaries"
|
|
11
11
|
|
|
12
|
-
# ---
|
|
12
|
+
# --- Guard: required file ---
|
|
13
13
|
if [[ ! -f "$MODEL_FILE" ]]; then
|
|
14
14
|
exit 0
|
|
15
15
|
fi
|
|
16
16
|
|
|
17
|
-
# --- Python
|
|
17
|
+
# --- Inline Python to parse JSON and analyze ---
|
|
18
18
|
python3 - <<'PYEOF'
|
|
19
19
|
import json
|
|
20
20
|
import sys
|
|
@@ -26,7 +26,7 @@ BRAIN_DIR = os.path.expanduser("~/.nexo/brain")
|
|
|
26
26
|
MODEL_FILE = os.path.join(BRAIN_DIR, "user_model.json")
|
|
27
27
|
SUMMARIES_DIR = os.path.join(BRAIN_DIR, "daily_summaries")
|
|
28
28
|
|
|
29
|
-
# ──
|
|
29
|
+
# ── Load model ──────────────────────────────────────────────────────────
|
|
30
30
|
try:
|
|
31
31
|
with open(MODEL_FILE) as f:
|
|
32
32
|
model = json.load(f)
|
|
@@ -35,11 +35,11 @@ except Exception:
|
|
|
35
35
|
|
|
36
36
|
insights = []
|
|
37
37
|
|
|
38
|
-
# ── 1.
|
|
38
|
+
# ── 1. Analyze evolution_log — last 3-5 entries ─────────────────────
|
|
39
39
|
evolution = model.get("evolution_log", [])
|
|
40
40
|
recent = evolution[-5:] if len(evolution) >= 5 else evolution
|
|
41
41
|
|
|
42
|
-
#
|
|
42
|
+
# Detect entries from the last 3 days
|
|
43
43
|
today = datetime.now().date()
|
|
44
44
|
cutoff = today - timedelta(days=3)
|
|
45
45
|
|
|
@@ -56,9 +56,9 @@ if recent_obs:
|
|
|
56
56
|
for obs in recent_obs[-2:]: # max 2 most recent
|
|
57
57
|
insights.append(obs["observation"].strip())
|
|
58
58
|
|
|
59
|
-
# ── 2.
|
|
60
|
-
#
|
|
61
|
-
#
|
|
59
|
+
# ── 2. Detect trait changes >0.1 between entries (if history exists) ──
|
|
60
|
+
# The current model only has a snapshot; if in the future there is history
|
|
61
|
+
# in evolution_log with trait deltas, this can be expanded here.
|
|
62
62
|
# Detect the most extreme trait as the dominant identity signal.
|
|
63
63
|
traits = model.get("traits", {})
|
|
64
64
|
if traits:
|
|
@@ -66,13 +66,13 @@ if traits:
|
|
|
66
66
|
dominant_val = traits[dominant]
|
|
67
67
|
weakest = min(traits, key=lambda k: traits[k])
|
|
68
68
|
weakest_val = traits[weakest]
|
|
69
|
-
#
|
|
69
|
+
# Only report if extreme (>0.9 or <0.2) to avoid noise
|
|
70
70
|
if dominant_val >= 0.9:
|
|
71
|
-
insights.append(f"
|
|
71
|
+
insights.append(f"Dominant trait: {dominant}={dominant_val} (highest recorded)")
|
|
72
72
|
if weakest_val <= 0.2:
|
|
73
|
-
insights.append(f"
|
|
73
|
+
insights.append(f"Lowest trait: {weakest}={weakest_val} (low tolerance active)")
|
|
74
74
|
|
|
75
|
-
# ── 3.
|
|
75
|
+
# ── 3. Recent contradictions (last 3 days) ─────────────────────────
|
|
76
76
|
contradictions = model.get("contradictions", [])
|
|
77
77
|
recent_contradictions = []
|
|
78
78
|
for c in contradictions:
|
|
@@ -84,24 +84,24 @@ for c in contradictions:
|
|
|
84
84
|
pass
|
|
85
85
|
|
|
86
86
|
for c in recent_contradictions:
|
|
87
|
-
insights.append(f"
|
|
87
|
+
insights.append(f"Contradiction detected: {c['description'].strip()}")
|
|
88
88
|
|
|
89
|
-
# ── 4.
|
|
89
|
+
# ── 4. Current active focus ─────────────────────────────────────────────────
|
|
90
90
|
current_focus = model.get("current_focus", [])
|
|
91
|
-
#
|
|
91
|
+
# Only include if there are focus items (and it's not initial startup)
|
|
92
92
|
if len(current_focus) > 0 and len(insights) == 0:
|
|
93
|
-
#
|
|
93
|
+
# If no other insights, report focus as minimum context
|
|
94
94
|
focus_str = ", ".join(current_focus)
|
|
95
|
-
insights.append(f"
|
|
95
|
+
insights.append(f"Current focus: {focus_str}")
|
|
96
96
|
|
|
97
|
-
# ── 5. Goals:
|
|
97
|
+
# ── 5. Goals: active/dormant changes ─────────────────────────────────────
|
|
98
98
|
goals_active = model.get("goals_active", [])
|
|
99
99
|
goals_dormant = model.get("goals_dormant", [])
|
|
100
|
-
#
|
|
100
|
+
# If there are dormant goals, mention them (may need reactivation)
|
|
101
101
|
if goals_dormant:
|
|
102
102
|
insights.append(f"Goal dormant: {goals_dormant[0]}")
|
|
103
103
|
|
|
104
|
-
# ──
|
|
104
|
+
# ── Read last daily summary ─────────────────────────────────────────────
|
|
105
105
|
summary_text = ""
|
|
106
106
|
try:
|
|
107
107
|
files = sorted(glob.glob(os.path.join(SUMMARIES_DIR, "*.md")))
|
|
@@ -109,17 +109,17 @@ try:
|
|
|
109
109
|
with open(files[-1]) as f:
|
|
110
110
|
lines = [l.rstrip() for l in f.readlines() if l.strip()]
|
|
111
111
|
# Take up to 3 lines of content (exclude header)
|
|
112
|
-
content_lines = [l for l in lines if not l.startswith("# Resumen")]
|
|
112
|
+
content_lines = [l for l in lines if not l.startswith("# Resumen") and not l.startswith("# Summary")]
|
|
113
113
|
summary_text = " | ".join(content_lines[:3])
|
|
114
114
|
except Exception:
|
|
115
115
|
pass
|
|
116
116
|
|
|
117
117
|
# ── Output ────────────────────────────────────────────────────────────────
|
|
118
|
-
#
|
|
118
|
+
# If no real insights, exit without output
|
|
119
119
|
if not insights and not summary_text:
|
|
120
120
|
sys.exit(0)
|
|
121
121
|
|
|
122
|
-
#
|
|
122
|
+
# Deduplicate and limit
|
|
123
123
|
seen = set()
|
|
124
124
|
unique_insights = []
|
|
125
125
|
for ins in insights:
|
|
@@ -27,15 +27,32 @@ from pathlib import Path
|
|
|
27
27
|
CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
|
|
28
28
|
|
|
29
29
|
HOME = Path.home()
|
|
30
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
30
31
|
LOG_DIR = NEXO_HOME / "logs"
|
|
31
32
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
33
|
LOG_FILE = LOG_DIR / "catchup.log"
|
|
33
34
|
STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
|
|
34
35
|
|
|
35
|
-
PYTHON_BREW = "/opt/homebrew/bin/python3"
|
|
36
|
-
PYTHON_SYS = "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3"
|
|
37
36
|
SCRIPTS = NEXO_HOME / "scripts"
|
|
38
37
|
|
|
38
|
+
# Resolve Python: prefer NEXO's venv, then the same Python running this script
|
|
39
|
+
def _resolve_python() -> str:
|
|
40
|
+
"""Find the best Python to use for subprocess calls."""
|
|
41
|
+
# Check for NEXO_CODE env var pointing to the repo's src/
|
|
42
|
+
nexo_code = os.environ.get("NEXO_CODE", "")
|
|
43
|
+
if nexo_code:
|
|
44
|
+
venv_python = Path(nexo_code).parent / ".venv" / "bin" / "python"
|
|
45
|
+
if venv_python.exists():
|
|
46
|
+
return str(venv_python)
|
|
47
|
+
# Check for venv relative to NEXO_HOME
|
|
48
|
+
venv_python = NEXO_HOME / ".venv" / "bin" / "python"
|
|
49
|
+
if venv_python.exists():
|
|
50
|
+
return str(venv_python)
|
|
51
|
+
# Fall back to the same Python running this script
|
|
52
|
+
return sys.executable
|
|
53
|
+
|
|
54
|
+
NEXO_PYTHON = _resolve_python()
|
|
55
|
+
|
|
39
56
|
|
|
40
57
|
def log(msg: str):
|
|
41
58
|
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
@@ -108,7 +125,7 @@ def run_task(name: str, python: str, script: str, state: dict) -> bool:
|
|
|
108
125
|
try:
|
|
109
126
|
result = subprocess.run(
|
|
110
127
|
[python, script_path],
|
|
111
|
-
capture_output=True, text=True, timeout=
|
|
128
|
+
capture_output=True, text=True, timeout=21600,
|
|
112
129
|
env={**os.environ, "HOME": str(HOME), "NEXO_CATCHUP": "1"}
|
|
113
130
|
)
|
|
114
131
|
if result.returncode == 0:
|
|
@@ -133,27 +150,15 @@ def main():
|
|
|
133
150
|
state = load_state()
|
|
134
151
|
|
|
135
152
|
# Define tasks in execution order (matching their intended schedule order)
|
|
136
|
-
#
|
|
137
|
-
update_script = SCRIPTS / "nexo-auto-update.py"
|
|
138
|
-
if update_script.exists():
|
|
139
|
-
log("Checking for NEXO updates...")
|
|
140
|
-
try:
|
|
141
|
-
subprocess.run(
|
|
142
|
-
[PYTHON_BREW if os.path.exists(PYTHON_BREW) else PYTHON_SYS, str(update_script)],
|
|
143
|
-
capture_output=True, text=True, timeout=60,
|
|
144
|
-
env={**os.environ, "HOME": str(HOME), "NEXO_HOME": str(NEXO_HOME / "nexo-mcp")}
|
|
145
|
-
)
|
|
146
|
-
except Exception as e:
|
|
147
|
-
log(f" Update check failed: {e}")
|
|
148
|
-
|
|
153
|
+
# Note: auto-update is handled by the MCP server on startup, not by catchup.
|
|
149
154
|
tasks = [
|
|
150
155
|
# (name, hour, minute, python, script, weekday)
|
|
151
|
-
("cognitive-decay", 3, 0,
|
|
152
|
-
("evolution", 3, 0,
|
|
153
|
-
("sleep", 4, 0,
|
|
154
|
-
("self-audit", 7, 0,
|
|
155
|
-
("github-monitor", 8, 0,
|
|
156
|
-
("postmortem", 23, 30,
|
|
156
|
+
("cognitive-decay", 3, 0, NEXO_PYTHON, "nexo-cognitive-decay.py", None),
|
|
157
|
+
("evolution", 3, 0, NEXO_PYTHON, "nexo-evolution-run.py", 6), # Sunday = 6
|
|
158
|
+
("sleep", 4, 0, NEXO_PYTHON, "nexo-sleep.py", None),
|
|
159
|
+
("self-audit", 7, 0, NEXO_PYTHON, "nexo-daily-self-audit.py", None),
|
|
160
|
+
("github-monitor", 8, 0, NEXO_PYTHON, "nexo-github-monitor.py", None),
|
|
161
|
+
("postmortem", 23, 30, NEXO_PYTHON, "nexo-postmortem-consolidator.py", None),
|
|
157
162
|
]
|
|
158
163
|
|
|
159
164
|
ran = 0
|
|
@@ -182,6 +187,11 @@ def _cli_post_catchup_assessment(ran: int, skipped: int, state: dict):
|
|
|
182
187
|
if not CLAUDE_CLI.exists():
|
|
183
188
|
log(f"Caught up {ran} tasks, {skipped} already current. (CLI unavailable for assessment)")
|
|
184
189
|
return
|
|
190
|
+
)
|
|
191
|
+
if auth_check.returncode != 0:
|
|
192
|
+
# CLI not authenticated, skip gracefully
|
|
193
|
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] Claude CLI not authenticated. Skipping CLI analysis.")
|
|
194
|
+
return
|
|
185
195
|
|
|
186
196
|
assessment_file = LOG_DIR / "catchup-assessment.md"
|
|
187
197
|
state_summary = json.dumps(state, indent=2, default=str)
|
|
@@ -208,14 +218,15 @@ Format:
|
|
|
208
218
|
|
|
209
219
|
log(f"Caught up {ran} tasks — running CLI assessment...")
|
|
210
220
|
env = os.environ.copy()
|
|
221
|
+
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
211
222
|
env.pop("CLAUDECODE", None)
|
|
212
223
|
env.pop("CLAUDE_CODE", None)
|
|
213
224
|
|
|
214
225
|
try:
|
|
215
226
|
result = subprocess.run(
|
|
216
|
-
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
|
|
217
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep"],
|
|
218
|
-
capture_output=True, text=True, timeout=
|
|
227
|
+
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus", "--output-format", "text",
|
|
228
|
+
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
|
|
229
|
+
capture_output=True, text=True, timeout=21600, env=env
|
|
219
230
|
)
|
|
220
231
|
if result.returncode == 0:
|
|
221
232
|
log(f"Assessment written to {assessment_file}")
|