nexo-brain 2.3.0 → 2.3.2
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 +1 -1
- package/bin/nexo-brain.js +92 -9
- package/bin/postinstall.js +22 -15
- package/package.json +7 -4
- package/src/auto_update.py +194 -5
- package/src/crons/sync.py +6 -2
- package/src/db/_core.py +1 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +1 -0
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +11 -1
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +1 -0
- package/src/hooks/capture-tool-logs.sh +23 -6
- package/src/hooks/session-start.sh +4 -3
- package/src/plugin_loader.py +1 -0
- package/src/plugins/update.py +377 -26
- package/src/scripts/deep-sleep/apply_findings.py +1 -0
- package/src/scripts/deep-sleep/collect.py +1 -0
- package/src/scripts/deep-sleep/extract.py +1 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-catchup.py +29 -4
- package/src/scripts/nexo-daily-self-audit.py +21 -1
- package/src/scripts/nexo-evolution-run.py +21 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -0
- package/src/scripts/nexo-postmortem-consolidator.py +34 -9
- package/src/scripts/nexo-sleep.py +32 -10
- package/src/scripts/nexo-synthesis.py +29 -9
- package/src/scripts/nexo-update.sh +109 -7
- package/src/scripts/nexo-watchdog.sh +122 -58
- package/src/server.py +66 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/nexo-preflight.sh +0 -236
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- package/tests/test_migrations.py +0 -137
|
@@ -1,597 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
NEXO Evolution — Standalone weekly runner with real execution.
|
|
4
|
-
Cron: 0 3 * * 0 (Sundays 3:00 AM)
|
|
5
|
-
|
|
6
|
-
Runs independently of Cortex. Calls Opus API directly to analyze
|
|
7
|
-
the past week and generate improvement proposals.
|
|
8
|
-
|
|
9
|
-
AUTO proposals are executed: snapshot → apply → validate → commit/rollback.
|
|
10
|
-
PROPOSE proposals are logged for the user's review.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
import os
|
|
15
|
-
import py_compile
|
|
16
|
-
import sqlite3
|
|
17
|
-
import subprocess
|
|
18
|
-
import sys
|
|
19
|
-
from datetime import datetime, date, timedelta
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
|
|
22
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
23
|
-
# Auto-detect: if running from repo (src/scripts/), use src/ as NEXO_CODE
|
|
24
|
-
_script_dir = Path(__file__).resolve().parent
|
|
25
|
-
_repo_src = _script_dir.parent # src/scripts/ -> src/
|
|
26
|
-
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
|
|
27
|
-
|
|
28
|
-
# ── Paths ────────────────────────────────────────────────────────────────
|
|
29
|
-
CLAUDE_DIR = NEXO_HOME
|
|
30
|
-
NEXO_DB = CLAUDE_DIR / "data" / "nexo.db"
|
|
31
|
-
LOG_DIR = CLAUDE_DIR / "logs"
|
|
32
|
-
SNAPSHOTS_DIR = CLAUDE_DIR / "snapshots"
|
|
33
|
-
SANDBOX_DIR = CLAUDE_DIR / "sandbox" / "workspace"
|
|
34
|
-
MAX_CONSECUTIVE_FAILURES = 3
|
|
35
|
-
MAX_SNAPSHOTS = 8
|
|
36
|
-
|
|
37
|
-
# ── Safe zones for AUTO execution ────────────────────────────────────────
|
|
38
|
-
# "review" mode (owner): broader zones, but nothing executes without approval
|
|
39
|
-
# "auto" mode (public users): restricted to user scripts and plugins ONLY
|
|
40
|
-
AUTO_SAFE_PREFIXES = [
|
|
41
|
-
str(CLAUDE_DIR / "scripts") + "/",
|
|
42
|
-
str(CLAUDE_DIR / "brain") + "/",
|
|
43
|
-
str(NEXO_CODE / "plugins") + "/",
|
|
44
|
-
str(CLAUDE_DIR / "logs") + "/",
|
|
45
|
-
str(CLAUDE_DIR / "coordination") + "/",
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
# Public mode: only user-created scripts — NEVER core, cortex, or plugins
|
|
49
|
-
AUTO_SAFE_PREFIXES_PUBLIC = [
|
|
50
|
-
str(CLAUDE_DIR / "scripts") + "/",
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
# ── Immutable files — NEVER touch (applies to ALL modes) ────────────────
|
|
54
|
-
IMMUTABLE_FILES = {
|
|
55
|
-
"db.py", "server.py", "plugin_loader.py", "nexo-watchdog.sh",
|
|
56
|
-
"cortex-wrapper.py", "CLAUDE.md", "personality.md",
|
|
57
|
-
"user-profile.md", "evolution_cycle.py",
|
|
58
|
-
# Core cognitive engine — never auto-modified
|
|
59
|
-
"cognitive.py", "knowledge_graph.py", "storage_router.py",
|
|
60
|
-
# Core tools — never auto-modified
|
|
61
|
-
"tools_sessions.py", "tools_coordination.py", "tools_reminders.py",
|
|
62
|
-
"tools_reminders_crud.py", "tools_learnings.py", "tools_credentials.py",
|
|
63
|
-
"tools_task_history.py", "tools_menu.py",
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
# ── Claude CLI path ──────────────────────────────────────────────────────
|
|
67
|
-
CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
|
|
68
|
-
|
|
69
|
-
# ── Logging ──────────────────────────────────────────────────────────────
|
|
70
|
-
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
71
|
-
LOG_FILE = LOG_DIR / "evolution.log"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def log(msg: str):
|
|
75
|
-
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
76
|
-
line = f"[{ts}] {msg}"
|
|
77
|
-
print(line, flush=True)
|
|
78
|
-
with open(LOG_FILE, "a") as f:
|
|
79
|
-
f.write(line + "\n")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# ── Import from evolution_cycle.py (lives in NEXO_CODE, i.e. src/) ──────
|
|
83
|
-
sys.path.insert(0, str(NEXO_CODE))
|
|
84
|
-
from evolution_cycle import (
|
|
85
|
-
load_objective, save_objective, get_week_data, build_evolution_prompt,
|
|
86
|
-
dry_run_restore_test, max_auto_changes, create_snapshot
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# ── Consecutive failure tracking ─────────────────────────────────────────
|
|
91
|
-
def get_consecutive_failures() -> int:
|
|
92
|
-
obj = load_objective()
|
|
93
|
-
return obj.get("consecutive_failures", 0)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def set_consecutive_failures(count: int):
|
|
97
|
-
obj = load_objective()
|
|
98
|
-
obj["consecutive_failures"] = count
|
|
99
|
-
save_objective(obj)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# ── Claude CLI call ──────────────────────────────────────────────────────
|
|
103
|
-
CLI_TIMEOUT = 21600 # 3h safety net (prevents zombie processes)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def verify_claude_cli() -> bool:
|
|
107
|
-
def call_claude_cli(prompt: str) -> str:
|
|
108
|
-
"""Call claude -p prompt --model opus via subprocess. Returns stdout text."""
|
|
109
|
-
env = os.environ.copy()
|
|
110
|
-
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
111
|
-
env.pop("CLAUDECODE", None)
|
|
112
|
-
env.pop("CLAUDE_CODE", None)
|
|
113
|
-
|
|
114
|
-
result = subprocess.run(
|
|
115
|
-
[str(CLAUDE_CLI), "-p", prompt, "--model", "opus",
|
|
116
|
-
"--output-format", "text",
|
|
117
|
-
"--allowedTools", "Read,Write,Edit,Glob,Grep,Bash,mcp__nexo__*"],
|
|
118
|
-
capture_output=True,
|
|
119
|
-
text=True,
|
|
120
|
-
timeout=CLI_TIMEOUT,
|
|
121
|
-
env=env,
|
|
122
|
-
)
|
|
123
|
-
if result.returncode != 0:
|
|
124
|
-
raise RuntimeError(f"claude CLI exited {result.returncode}: {result.stderr[:500]}")
|
|
125
|
-
return result.stdout
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
# ── File safety validation ───────────────────────────────────────────────
|
|
129
|
-
def is_safe_path(filepath: str, mode: str = "auto") -> bool:
|
|
130
|
-
"""Check if a file path is within safe zones and not immutable.
|
|
131
|
-
mode='auto' (public): restricted to scripts/ and plugins/ only.
|
|
132
|
-
mode='review' (owner): broader zones but nothing executes without approval anyway.
|
|
133
|
-
"""
|
|
134
|
-
expanded = str(Path(filepath).expanduser().resolve())
|
|
135
|
-
filename = Path(expanded).name
|
|
136
|
-
|
|
137
|
-
if filename in IMMUTABLE_FILES:
|
|
138
|
-
return False
|
|
139
|
-
|
|
140
|
-
prefixes = AUTO_SAFE_PREFIXES if mode == "review" else AUTO_SAFE_PREFIXES_PUBLIC
|
|
141
|
-
for prefix in prefixes:
|
|
142
|
-
resolved_prefix = str(Path(prefix).expanduser().resolve())
|
|
143
|
-
if expanded.startswith(resolved_prefix):
|
|
144
|
-
return True
|
|
145
|
-
|
|
146
|
-
return False
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def validate_syntax(filepath: str) -> tuple[bool, str]:
|
|
150
|
-
"""Basic syntax validation for known file types."""
|
|
151
|
-
path = Path(filepath)
|
|
152
|
-
ext = path.suffix
|
|
153
|
-
|
|
154
|
-
if ext == ".py":
|
|
155
|
-
try:
|
|
156
|
-
py_compile.compile(str(path), doraise=True)
|
|
157
|
-
return True, "Python syntax OK"
|
|
158
|
-
except Exception as e:
|
|
159
|
-
return False, f"Validation error: {e}"
|
|
160
|
-
|
|
161
|
-
elif ext == ".sh":
|
|
162
|
-
try:
|
|
163
|
-
result = subprocess.run(
|
|
164
|
-
["bash", "-n", str(path)],
|
|
165
|
-
capture_output=True, text=True, timeout=10
|
|
166
|
-
)
|
|
167
|
-
if result.returncode == 0:
|
|
168
|
-
return True, "Bash syntax OK"
|
|
169
|
-
return False, f"Bash syntax error: {result.stderr[:200]}"
|
|
170
|
-
except Exception as e:
|
|
171
|
-
return False, f"Validation error: {e}"
|
|
172
|
-
|
|
173
|
-
elif ext == ".json":
|
|
174
|
-
try:
|
|
175
|
-
json.loads(Path(filepath).read_text())
|
|
176
|
-
return True, "JSON valid"
|
|
177
|
-
except Exception as e:
|
|
178
|
-
return False, f"JSON error: {e}"
|
|
179
|
-
|
|
180
|
-
elif ext == ".md":
|
|
181
|
-
return True, "Markdown (no validation needed)"
|
|
182
|
-
|
|
183
|
-
return True, f"No validator for {ext} (accepted)"
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# ── Apply a single change operation ──────────────────────────────────────
|
|
187
|
-
def apply_change(change: dict) -> tuple[bool, str]:
|
|
188
|
-
"""Apply a single file change operation. Returns (success, message)."""
|
|
189
|
-
filepath = str(Path(change["file"]).expanduser())
|
|
190
|
-
operation = change.get("operation", "")
|
|
191
|
-
content = change.get("content", "")
|
|
192
|
-
|
|
193
|
-
if not is_safe_path(filepath):
|
|
194
|
-
return False, f"BLOCKED: {filepath} is outside safe zones or immutable"
|
|
195
|
-
|
|
196
|
-
try:
|
|
197
|
-
if operation == "create":
|
|
198
|
-
if Path(filepath).exists():
|
|
199
|
-
return False, f"BLOCKED: {filepath} already exists (create requires new file)"
|
|
200
|
-
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
-
Path(filepath).write_text(content)
|
|
202
|
-
# Make scripts executable
|
|
203
|
-
if filepath.endswith(".sh") or filepath.endswith(".py"):
|
|
204
|
-
os.chmod(filepath, 0o755)
|
|
205
|
-
return True, f"Created {filepath}"
|
|
206
|
-
|
|
207
|
-
elif operation == "replace":
|
|
208
|
-
search = change.get("search", "")
|
|
209
|
-
if not search:
|
|
210
|
-
return False, "BLOCKED: replace operation requires 'search' field"
|
|
211
|
-
if not Path(filepath).exists():
|
|
212
|
-
return False, f"BLOCKED: {filepath} does not exist"
|
|
213
|
-
original = Path(filepath).read_text()
|
|
214
|
-
count = original.count(search)
|
|
215
|
-
if count == 0:
|
|
216
|
-
return False, f"BLOCKED: search text not found in {filepath}"
|
|
217
|
-
if count > 1:
|
|
218
|
-
return False, f"BLOCKED: search text matches {count} times (must be unique)"
|
|
219
|
-
new_content = original.replace(search, content, 1)
|
|
220
|
-
Path(filepath).write_text(new_content)
|
|
221
|
-
return True, f"Replaced in {filepath}"
|
|
222
|
-
|
|
223
|
-
elif operation == "append":
|
|
224
|
-
if not Path(filepath).exists():
|
|
225
|
-
return False, f"BLOCKED: {filepath} does not exist"
|
|
226
|
-
with open(filepath, "a") as f:
|
|
227
|
-
f.write(content)
|
|
228
|
-
return True, f"Appended to {filepath}"
|
|
229
|
-
|
|
230
|
-
else:
|
|
231
|
-
return False, f"BLOCKED: unknown operation '{operation}'"
|
|
232
|
-
|
|
233
|
-
except Exception as e:
|
|
234
|
-
return False, f"ERROR: {e}"
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
# ── Execute AUTO proposals ───────────────────────────────────────────────
|
|
238
|
-
def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connection) -> dict:
|
|
239
|
-
"""Execute an AUTO proposal with snapshot/apply/validate/rollback."""
|
|
240
|
-
changes = proposal.get("changes", [])
|
|
241
|
-
if not changes:
|
|
242
|
-
return {"status": "skipped", "reason": "No changes array in proposal"}
|
|
243
|
-
|
|
244
|
-
# Validate all paths first
|
|
245
|
-
for change in changes:
|
|
246
|
-
filepath = str(Path(change["file"]).expanduser())
|
|
247
|
-
if not is_safe_path(filepath):
|
|
248
|
-
return {"status": "blocked", "reason": f"Unsafe path: {filepath}"}
|
|
249
|
-
|
|
250
|
-
# Collect files to snapshot (existing files only)
|
|
251
|
-
files_to_backup = []
|
|
252
|
-
for change in changes:
|
|
253
|
-
filepath = str(Path(change["file"]).expanduser())
|
|
254
|
-
if Path(filepath).exists():
|
|
255
|
-
files_to_backup.append(filepath)
|
|
256
|
-
|
|
257
|
-
# Create snapshot
|
|
258
|
-
snapshot_ref = None
|
|
259
|
-
if files_to_backup:
|
|
260
|
-
snapshot_ref = create_snapshot(files_to_backup)
|
|
261
|
-
log(f" Snapshot created: {snapshot_ref}")
|
|
262
|
-
|
|
263
|
-
# Apply changes
|
|
264
|
-
applied_files = []
|
|
265
|
-
all_results = []
|
|
266
|
-
try:
|
|
267
|
-
for change in changes:
|
|
268
|
-
success, msg = apply_change(change)
|
|
269
|
-
all_results.append(msg)
|
|
270
|
-
log(f" {msg}")
|
|
271
|
-
if not success:
|
|
272
|
-
raise RuntimeError(f"Change failed: {msg}")
|
|
273
|
-
filepath = str(Path(change["file"]).expanduser())
|
|
274
|
-
applied_files.append(filepath)
|
|
275
|
-
|
|
276
|
-
# Validate all modified/created files
|
|
277
|
-
for filepath in applied_files:
|
|
278
|
-
valid, vmsg = validate_syntax(filepath)
|
|
279
|
-
all_results.append(vmsg)
|
|
280
|
-
log(f" Validate: {vmsg}")
|
|
281
|
-
if not valid:
|
|
282
|
-
raise RuntimeError(f"Validation failed: {vmsg}")
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
"status": "applied",
|
|
286
|
-
"snapshot_ref": snapshot_ref,
|
|
287
|
-
"files_changed": applied_files,
|
|
288
|
-
"test_result": "; ".join(all_results),
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
except RuntimeError as e:
|
|
292
|
-
# Rollback
|
|
293
|
-
log(f" ROLLBACK: {e}")
|
|
294
|
-
if snapshot_ref:
|
|
295
|
-
try:
|
|
296
|
-
restore_script = CLAUDE_DIR / "scripts" / "nexo-snapshot-restore.sh"
|
|
297
|
-
subprocess.run(
|
|
298
|
-
[str(restore_script), snapshot_ref],
|
|
299
|
-
capture_output=True, timeout=15, check=True
|
|
300
|
-
)
|
|
301
|
-
log(f" Restored from snapshot {snapshot_ref}")
|
|
302
|
-
except Exception as re:
|
|
303
|
-
log(f" CRITICAL: Restore failed: {re}")
|
|
304
|
-
else:
|
|
305
|
-
# Remove created files that didn't exist before
|
|
306
|
-
for filepath in applied_files:
|
|
307
|
-
if filepath not in files_to_backup:
|
|
308
|
-
Path(filepath).unlink(missing_ok=True)
|
|
309
|
-
log(f" Removed created file: {filepath}")
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
"status": "failed",
|
|
313
|
-
"snapshot_ref": snapshot_ref,
|
|
314
|
-
"files_changed": [],
|
|
315
|
-
"test_result": f"ROLLBACK: {e}; " + "; ".join(all_results),
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
# ── Review followup for owner mode ──────────────────────────────────────
|
|
320
|
-
def _create_review_followup(conn: sqlite3.Connection, cycle_num: int,
|
|
321
|
-
items: list[dict], analysis: str):
|
|
322
|
-
"""Create a followup summarizing Evolution proposals for owner review."""
|
|
323
|
-
tomorrow = (date.today() + timedelta(days=1)).isoformat()
|
|
324
|
-
followup_id = f"NF-EVO-C{cycle_num}"
|
|
325
|
-
|
|
326
|
-
public_items = [i for i in items if i.get("scope") == "public"]
|
|
327
|
-
local_items = [i for i in items if i.get("scope") != "public"]
|
|
328
|
-
|
|
329
|
-
lines = [f"Evolution Cycle #{cycle_num} — {len(items)} proposals to review."]
|
|
330
|
-
lines.append(f"Analysis: {analysis[:200]}")
|
|
331
|
-
lines.append("")
|
|
332
|
-
|
|
333
|
-
if public_items:
|
|
334
|
-
lines.append(f"FOR EVERYONE ({len(public_items)}):")
|
|
335
|
-
for i, item in enumerate(public_items, 1):
|
|
336
|
-
lines.append(f" {i}. [{item['dimension']}] {item['action'][:120]}")
|
|
337
|
-
lines.append(f" Why: {item['reasoning'][:100]}")
|
|
338
|
-
lines.append("")
|
|
339
|
-
|
|
340
|
-
if local_items:
|
|
341
|
-
lines.append(f"FOR YOU ONLY ({len(local_items)}):")
|
|
342
|
-
for i, item in enumerate(local_items, 1):
|
|
343
|
-
lines.append(f" {i}. [{item['dimension']}] {item['action'][:120]}")
|
|
344
|
-
lines.append(f" Why: {item['reasoning'][:100]}")
|
|
345
|
-
|
|
346
|
-
description = "\n".join(lines)
|
|
347
|
-
|
|
348
|
-
try:
|
|
349
|
-
now_epoch = datetime.now().timestamp()
|
|
350
|
-
conn.execute(
|
|
351
|
-
"INSERT OR REPLACE INTO followups (id, description, date, status, verification, created_at, updated_at) "
|
|
352
|
-
"VALUES (?, ?, ?, 'pending', ?, ?, ?)",
|
|
353
|
-
(followup_id, description, tomorrow,
|
|
354
|
-
f"SELECT * FROM evolution_log WHERE cycle_number={cycle_num}",
|
|
355
|
-
now_epoch, now_epoch)
|
|
356
|
-
)
|
|
357
|
-
conn.commit()
|
|
358
|
-
log(f" Followup {followup_id} created for {tomorrow}")
|
|
359
|
-
except Exception as e:
|
|
360
|
-
log(f" WARN: Failed to create followup: {e}")
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
# ── Main run ─────────────────────────────────────────────────────────────
|
|
364
|
-
def run():
|
|
365
|
-
log("=" * 60)
|
|
366
|
-
log("NEXO Evolution cycle starting (standalone, v2 — real execution)")
|
|
367
|
-
|
|
368
|
-
# Check objective
|
|
369
|
-
objective = load_objective()
|
|
370
|
-
if not objective:
|
|
371
|
-
log("ERROR: No evolution-objective.json found")
|
|
372
|
-
sys.exit(1)
|
|
373
|
-
if not objective.get("evolution_enabled", True):
|
|
374
|
-
log(f"Evolution DISABLED: {objective.get('disabled_reason', 'unknown')}")
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
# Circuit breaker: consecutive failures
|
|
378
|
-
failures = get_consecutive_failures()
|
|
379
|
-
if failures >= MAX_CONSECUTIVE_FAILURES:
|
|
380
|
-
log(f"CIRCUIT BREAKER: {failures} consecutive failures. Disabling evolution.")
|
|
381
|
-
objective["evolution_enabled"] = False
|
|
382
|
-
objective["disabled_reason"] = f"Circuit breaker: {failures} consecutive failures at {datetime.now().isoformat()}"
|
|
383
|
-
save_objective(objective)
|
|
384
|
-
return
|
|
385
|
-
|
|
386
|
-
# Dry-run restore test
|
|
387
|
-
log("Running restore dry-run test...")
|
|
388
|
-
if not dry_run_restore_test():
|
|
389
|
-
log("CRITICAL: Restore test failed — aborting")
|
|
390
|
-
set_consecutive_failures(failures + 1)
|
|
391
|
-
sys.exit(1)
|
|
392
|
-
log("Restore test PASSED")
|
|
393
|
-
|
|
394
|
-
# Gather data
|
|
395
|
-
log("Gathering week data from nexo.db...")
|
|
396
|
-
week_data = get_week_data(str(NEXO_DB))
|
|
397
|
-
log(f" Learnings: {len(week_data.get('learnings', []))}")
|
|
398
|
-
log(f" Decisions: {len(week_data.get('decisions', []))}")
|
|
399
|
-
log(f" Changes: {len(week_data.get('changes', []))}")
|
|
400
|
-
log(f" Diaries: {len(week_data.get('diaries', []))}")
|
|
401
|
-
|
|
402
|
-
# Build prompt
|
|
403
|
-
prompt = build_evolution_prompt(week_data, objective)
|
|
404
|
-
log(f"Prompt built: {len(prompt)} chars")
|
|
405
|
-
|
|
406
|
-
# Verify Claude CLI is authenticated before calling
|
|
407
|
-
if not verify_claude_cli():
|
|
408
|
-
log("Claude CLI not available or not authenticated. Skipping evolution run.")
|
|
409
|
-
return
|
|
410
|
-
|
|
411
|
-
# Call Opus via claude -p
|
|
412
|
-
log("Calling claude -p --model opus...")
|
|
413
|
-
try:
|
|
414
|
-
raw_response = call_claude_cli(prompt)
|
|
415
|
-
except Exception as e:
|
|
416
|
-
log(f"claude CLI call failed: {e}")
|
|
417
|
-
set_consecutive_failures(failures + 1)
|
|
418
|
-
return
|
|
419
|
-
|
|
420
|
-
log(f"Response received: {len(raw_response)} chars")
|
|
421
|
-
|
|
422
|
-
# Parse JSON
|
|
423
|
-
try:
|
|
424
|
-
text = raw_response
|
|
425
|
-
if "```json" in text:
|
|
426
|
-
text = text.split("```json")[1].split("```")[0]
|
|
427
|
-
elif "```" in text:
|
|
428
|
-
text = text.split("```")[1].split("```")[0]
|
|
429
|
-
response = json.loads(text.strip())
|
|
430
|
-
except Exception as e:
|
|
431
|
-
log(f"JSON parse failed: {e}")
|
|
432
|
-
log(f"Raw (first 500): {raw_response[:500]}")
|
|
433
|
-
set_consecutive_failures(failures + 1)
|
|
434
|
-
return
|
|
435
|
-
|
|
436
|
-
# Reset consecutive failures on successful parse
|
|
437
|
-
set_consecutive_failures(0)
|
|
438
|
-
|
|
439
|
-
log(f"Analysis: {response.get('analysis', 'N/A')[:200]}")
|
|
440
|
-
|
|
441
|
-
# Log patterns
|
|
442
|
-
for p in response.get("patterns", []):
|
|
443
|
-
log(f" Pattern [{p.get('type', '?')}]: {p.get('description', '')[:100]} (freq: {p.get('frequency', '?')})")
|
|
444
|
-
|
|
445
|
-
# Process proposals
|
|
446
|
-
proposals = response.get("proposals", [])
|
|
447
|
-
cycle_num = objective.get("total_evolutions", 0) + 1
|
|
448
|
-
max_auto = max_auto_changes(objective.get("total_evolutions", 0))
|
|
449
|
-
auto_count = 0
|
|
450
|
-
auto_applied = 0
|
|
451
|
-
evolution_mode = objective.get("evolution_mode", "auto") # "auto" (public) or "review" (owner)
|
|
452
|
-
|
|
453
|
-
conn = sqlite3.connect(str(NEXO_DB), timeout=10)
|
|
454
|
-
conn.execute("PRAGMA busy_timeout=5000")
|
|
455
|
-
|
|
456
|
-
# In "review" mode: log everything as pending_review, create followup
|
|
457
|
-
# In "auto" mode: execute AUTO proposals, log PROPOSE as proposed
|
|
458
|
-
review_items = []
|
|
459
|
-
|
|
460
|
-
for p in proposals:
|
|
461
|
-
classification = p.get("classification", "propose")
|
|
462
|
-
dimension = p.get("dimension", "other")
|
|
463
|
-
action = p.get("action", "")
|
|
464
|
-
reasoning = p.get("reasoning", "")
|
|
465
|
-
scope = p.get("scope", "local") # "public" or "local"
|
|
466
|
-
|
|
467
|
-
if evolution_mode == "review":
|
|
468
|
-
# Owner mode: nothing executes, everything queued for review
|
|
469
|
-
log(f" QUEUED [{scope}]: {action[:80]}")
|
|
470
|
-
conn.execute(
|
|
471
|
-
"INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
|
|
472
|
-
"reasoning, status) VALUES (?, ?, ?, ?, ?, ?)",
|
|
473
|
-
(cycle_num, dimension, action, classification, reasoning, "pending_review")
|
|
474
|
-
)
|
|
475
|
-
review_items.append({
|
|
476
|
-
"dimension": dimension,
|
|
477
|
-
"action": action,
|
|
478
|
-
"reasoning": reasoning,
|
|
479
|
-
"scope": scope,
|
|
480
|
-
"classification": classification,
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
elif classification == "auto" and auto_count < max_auto:
|
|
484
|
-
# Public mode: execute AUTO proposals
|
|
485
|
-
auto_count += 1
|
|
486
|
-
log(f" AUTO #{auto_count}/{max_auto}: {action[:80]}")
|
|
487
|
-
|
|
488
|
-
result = execute_auto_proposal(p, cycle_num, conn)
|
|
489
|
-
status = result["status"]
|
|
490
|
-
|
|
491
|
-
conn.execute(
|
|
492
|
-
"INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
|
|
493
|
-
"reasoning, status, files_changed, snapshot_ref, test_result) "
|
|
494
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
495
|
-
(cycle_num, dimension, action, "auto", reasoning, status,
|
|
496
|
-
json.dumps(result.get("files_changed", [])),
|
|
497
|
-
result.get("snapshot_ref", ""),
|
|
498
|
-
result.get("test_result", ""))
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
if status == "applied":
|
|
502
|
-
auto_applied += 1
|
|
503
|
-
log(f" APPLIED successfully")
|
|
504
|
-
elif status == "blocked":
|
|
505
|
-
log(f" BLOCKED: {result.get('test_result', '')}")
|
|
506
|
-
elif status == "skipped":
|
|
507
|
-
log(f" SKIPPED: {result.get('reason', '')}")
|
|
508
|
-
else:
|
|
509
|
-
log(f" FAILED: {result.get('test_result', '')[:100]}")
|
|
510
|
-
|
|
511
|
-
else:
|
|
512
|
-
# PROPOSE or over auto limit
|
|
513
|
-
if classification == "auto" and auto_count >= max_auto:
|
|
514
|
-
log(f" AUTO→PROPOSE (over limit {max_auto}): {action[:80]}")
|
|
515
|
-
classification = "propose"
|
|
516
|
-
else:
|
|
517
|
-
log(f" PROPOSE: {action[:80]}")
|
|
518
|
-
|
|
519
|
-
conn.execute(
|
|
520
|
-
"INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
|
|
521
|
-
"reasoning, status) VALUES (?, ?, ?, ?, ?, ?)",
|
|
522
|
-
(cycle_num, dimension, action, classification, reasoning, "proposed")
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
conn.commit()
|
|
526
|
-
|
|
527
|
-
# In review mode: create followup for owner
|
|
528
|
-
if evolution_mode == "review" and review_items:
|
|
529
|
-
_create_review_followup(conn, cycle_num, review_items, response.get("analysis", ""))
|
|
530
|
-
|
|
531
|
-
# Update metrics
|
|
532
|
-
scores = response.get("dimension_scores", {})
|
|
533
|
-
evidence = response.get("score_evidence", {})
|
|
534
|
-
current = week_data.get("current_metrics", {})
|
|
535
|
-
|
|
536
|
-
for dim, score in scores.items():
|
|
537
|
-
if isinstance(score, (int, float)) and 0 <= score <= 100:
|
|
538
|
-
prev = current.get(dim, {}).get("score", 0)
|
|
539
|
-
delta = int(score) - prev
|
|
540
|
-
conn.execute(
|
|
541
|
-
"INSERT INTO evolution_metrics (dimension, score, evidence, delta) VALUES (?, ?, ?, ?)",
|
|
542
|
-
(dim, int(score), json.dumps(evidence.get(dim, "")), delta)
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
conn.commit()
|
|
546
|
-
conn.close()
|
|
547
|
-
|
|
548
|
-
# Update objective
|
|
549
|
-
objective["last_evolution"] = str(date.today())
|
|
550
|
-
objective["total_evolutions"] = cycle_num
|
|
551
|
-
objective["total_proposals_made"] = objective.get("total_proposals_made", 0) + len(proposals)
|
|
552
|
-
objective["total_auto_applied"] = objective.get("total_auto_applied", 0) + auto_applied
|
|
553
|
-
for dim, score in scores.items():
|
|
554
|
-
if dim in objective.get("dimensions", {}) and isinstance(score, (int, float)):
|
|
555
|
-
objective["dimensions"][dim]["current"] = int(score)
|
|
556
|
-
|
|
557
|
-
objective.setdefault("history", []).insert(0, {
|
|
558
|
-
"cycle": cycle_num,
|
|
559
|
-
"date": str(date.today()),
|
|
560
|
-
"proposals": len(proposals),
|
|
561
|
-
"auto_count": auto_count,
|
|
562
|
-
"auto_applied": auto_applied,
|
|
563
|
-
"analysis": response.get("analysis", "")[:200]
|
|
564
|
-
})
|
|
565
|
-
objective["history"] = objective["history"][:12]
|
|
566
|
-
|
|
567
|
-
save_objective(objective)
|
|
568
|
-
|
|
569
|
-
log(f"Evolution cycle #{cycle_num} COMPLETE: {len(proposals)} proposals "
|
|
570
|
-
f"({auto_count} auto, {auto_applied} applied, "
|
|
571
|
-
f"{len(proposals) - auto_count} propose)")
|
|
572
|
-
log("=" * 60)
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
def _update_catchup_state():
|
|
576
|
-
"""Register successful run for catch-up."""
|
|
577
|
-
try:
|
|
578
|
-
import json as _json
|
|
579
|
-
from pathlib import Path as _Path
|
|
580
|
-
|
|
581
|
-
_state_file = NEXO_HOME / "operations" / ".catchup-state.json"
|
|
582
|
-
_state = _json.loads(_state_file.read_text()) if _state_file.exists() else {}
|
|
583
|
-
_state["evolution"] = datetime.now().isoformat()
|
|
584
|
-
_state_file.write_text(_json.dumps(_state, indent=2))
|
|
585
|
-
except Exception:
|
|
586
|
-
pass
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if __name__ == "__main__":
|
|
590
|
-
try:
|
|
591
|
-
run()
|
|
592
|
-
_update_catchup_state()
|
|
593
|
-
except Exception as e:
|
|
594
|
-
log(f"FATAL: {e}")
|
|
595
|
-
import traceback
|
|
596
|
-
log(traceback.format_exc())
|
|
597
|
-
sys.exit(1)
|