nexo-brain 7.38.6 → 7.38.7
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 +2 -2
- package/README.md +7 -5
- package/package.json +2 -2
- package/src/auto_update.py +14 -22
- package/src/automation_supervisor.py +11 -11
- package/src/core_schedule_controls.py +3 -2
- package/src/crons/manifest.json +15 -0
- package/src/enforcement_engine.py +1 -1
- package/src/evolution_cycle.py +408 -0
- package/src/plugins/evolution.py +173 -26
- package/src/product_mode.py +31 -25
- package/src/public_contribution.py +1 -1
- package/src/public_evolution_queue.py +241 -0
- package/src/scripts/deep-sleep/apply_findings.py +67 -44
- package/src/scripts/nexo-daily-self-audit.py +12 -17
- package/src/scripts/nexo-evolution-run.py +1379 -0
- package/src/scripts/nexo-proactive-dashboard.py +5 -7
- package/src/scripts/nexo-runtime-preflight.py +130 -25
- package/src/scripts/nexo-watchdog-smoke.py +5 -7
- package/src/tools_drive.py +4 -4
- package/templates/core-prompts/evolution-public-contribution.md +32 -0
- package/templates/core-prompts/evolution-public-pr-review.md +38 -0
- package/templates/core-prompts/evolution-weekly.md +71 -0
- package/templates/launchagents/README.md +1 -0
- package/templates/launchagents/com.nexo.evolution.plist +44 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.38.
|
|
4
|
-
"description": "Local NEXO runtime core for NEXO Desktop: memory, Deep Sleep, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
3
|
+
"version": "7.38.7",
|
|
4
|
+
"description": "Local NEXO runtime core for NEXO Desktop: memory, Deep Sleep, Evolution support-ticket mode, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Cognitive S.L.",
|
|
7
7
|
"email": "info@nexo-desktop.com",
|
package/README.md
CHANGED
|
@@ -9,13 +9,15 @@ compatibility only; they are not a separate product line.
|
|
|
9
9
|
|
|
10
10
|
## Current contract
|
|
11
11
|
|
|
12
|
-
- Version `7.38.
|
|
12
|
+
- Version `7.38.7` is the current packaged-runtime line
|
|
13
13
|
- Public product: NEXO Desktop
|
|
14
14
|
- Runtime role: local NEXO core bundled with Desktop
|
|
15
|
-
- Active systems: local memory, Deep Sleep,
|
|
16
|
-
doctor diagnostics, and MCP tooling
|
|
17
|
-
-
|
|
18
|
-
|
|
15
|
+
- Active systems: local memory, Deep Sleep, Evolution support-ticket mode,
|
|
16
|
+
Skills, Watchdog, followups, doctor diagnostics, and MCP tooling
|
|
17
|
+
- Evolution mode: enabled by default for Desktop-managed installs; it never
|
|
18
|
+
opens GitHub branches, pushes, PRs, transcripts, local databases, or raw
|
|
19
|
+
private evidence, and routes product-improvement requests through sanitized
|
|
20
|
+
support tickets
|
|
19
21
|
- Distribution: Desktop installers and update manifests are published through
|
|
20
22
|
the NEXO Desktop release channel
|
|
21
23
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.38.
|
|
3
|
+
"version": "7.38.7",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
|
-
"description": "NEXO runtime core for NEXO Desktop. Provides local memory,
|
|
5
|
+
"description": "NEXO runtime core for NEXO Desktop. Provides local memory, Deep Sleep, Evolution support-ticket mode, skills, watchdog, and MCP tooling for the Desktop product.",
|
|
6
6
|
"homepage": "https://nexo-desktop.com",
|
|
7
7
|
"bin": {
|
|
8
8
|
"nexo-brain": "bin/nexo-brain.js",
|
package/src/auto_update.py
CHANGED
|
@@ -21,12 +21,8 @@ import time
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
23
|
try:
|
|
24
|
-
from product_mode import
|
|
25
|
-
|
|
26
|
-
DESKTOP_EVOLUTION_SUPPORT_MODE,
|
|
27
|
-
desktop_product_requested,
|
|
28
|
-
enforce_desktop_product_contract,
|
|
29
|
-
)
|
|
24
|
+
from product_mode import desktop_product_requested, enforce_desktop_product_contract
|
|
25
|
+
from product_mode import DESKTOP_EVOLUTION_SUPPORT_MODE
|
|
30
26
|
except ModuleNotFoundError as exc:
|
|
31
27
|
if getattr(exc, "name", "") != "product_mode":
|
|
32
28
|
raise
|
|
@@ -35,12 +31,8 @@ except ModuleNotFoundError as exc:
|
|
|
35
31
|
core_path = str(_core_runtime)
|
|
36
32
|
if core_path not in sys.path:
|
|
37
33
|
sys.path.insert(0, core_path)
|
|
38
|
-
from product_mode import
|
|
39
|
-
|
|
40
|
-
DESKTOP_EVOLUTION_SUPPORT_MODE,
|
|
41
|
-
desktop_product_requested,
|
|
42
|
-
enforce_desktop_product_contract,
|
|
43
|
-
)
|
|
34
|
+
from product_mode import desktop_product_requested, enforce_desktop_product_contract
|
|
35
|
+
from product_mode import DESKTOP_EVOLUTION_SUPPORT_MODE
|
|
44
36
|
from runtime_home import export_resolved_nexo_home, managed_nexo_home
|
|
45
37
|
|
|
46
38
|
try:
|
|
@@ -1441,7 +1433,6 @@ def _cleanup_retired_runtime_files():
|
|
|
1441
1433
|
"""Remove retired core files that should not survive updates."""
|
|
1442
1434
|
retired = [
|
|
1443
1435
|
paths.core_scripts_dir() / "nexo-day-orchestrator.sh",
|
|
1444
|
-
paths.core_scripts_dir() / "nexo-evolution-run.py",
|
|
1445
1436
|
paths.core_scripts_dir() / "heartbeat-enforcement.py",
|
|
1446
1437
|
paths.core_scripts_dir() / "heartbeat-posttool.sh",
|
|
1447
1438
|
paths.core_scripts_dir() / "heartbeat-user-msg.sh",
|
|
@@ -4383,28 +4374,29 @@ def _auto_update_check_locked() -> dict:
|
|
|
4383
4374
|
except Exception as e:
|
|
4384
4375
|
_log(f"File migration runner error: {e}")
|
|
4385
4376
|
|
|
4386
|
-
# Legacy cleanup:
|
|
4387
|
-
#
|
|
4377
|
+
# Legacy cleanup: keep the objective but migrate old Desktop-disabled
|
|
4378
|
+
# states back to the support-ticket-only Evolution mode.
|
|
4388
4379
|
try:
|
|
4389
4380
|
evo_obj_path = paths.brain_dir() / "evolution-objective.json"
|
|
4390
4381
|
if evo_obj_path.exists():
|
|
4391
4382
|
raw_objective = json.loads(evo_obj_path.read_text())
|
|
4392
4383
|
if isinstance(raw_objective, dict):
|
|
4393
|
-
raw_objective["evolution_enabled"] =
|
|
4384
|
+
raw_objective["evolution_enabled"] = True
|
|
4394
4385
|
raw_objective["evolution_mode"] = DESKTOP_EVOLUTION_SUPPORT_MODE
|
|
4395
|
-
raw_objective["
|
|
4396
|
-
raw_objective["
|
|
4397
|
-
raw_objective
|
|
4398
|
-
raw_objective
|
|
4386
|
+
raw_objective["desktop_managed"] = True
|
|
4387
|
+
raw_objective["support_ticket_mode"] = True
|
|
4388
|
+
raw_objective.pop("disabled_by", None)
|
|
4389
|
+
raw_objective.pop("disabled_reason", None)
|
|
4390
|
+
raw_objective.pop("removed_at", None)
|
|
4399
4391
|
evo_obj_path.write_text(json.dumps(raw_objective, indent=2, ensure_ascii=False) + "\n")
|
|
4400
|
-
_log("
|
|
4392
|
+
_log("Migrated evolution-objective.json to support-ticket mode")
|
|
4401
4393
|
except Exception as e:
|
|
4402
4394
|
_log(f"legacy evolution cleanup error: {e}")
|
|
4403
4395
|
|
|
4404
4396
|
try:
|
|
4405
4397
|
desktop_contract = enforce_desktop_product_contract(source="auto_update")
|
|
4406
4398
|
if desktop_contract.get("applied") and desktop_contract.get("changed_objective"):
|
|
4407
|
-
_log("Desktop product contract enforced:
|
|
4399
|
+
_log("Desktop product contract enforced: Evolution support-ticket mode")
|
|
4408
4400
|
except Exception as e:
|
|
4409
4401
|
_log(f"desktop product contract error: {e}")
|
|
4410
4402
|
|
|
@@ -388,30 +388,30 @@ def classify_evolution_policy(
|
|
|
388
388
|
break
|
|
389
389
|
if not evolution_entry:
|
|
390
390
|
return EvolutionPolicyClassification(
|
|
391
|
-
status="
|
|
392
|
-
severity="
|
|
393
|
-
reason="Evolution
|
|
391
|
+
status="missing",
|
|
392
|
+
severity="P1",
|
|
393
|
+
reason="Evolution is enabled by product policy but missing from the cron manifest",
|
|
394
394
|
)
|
|
395
395
|
label = str(evolution_entry.get("launchagent_label") or "com.nexo.evolution")
|
|
396
396
|
if launchagent_labels is None:
|
|
397
397
|
return EvolutionPolicyClassification(
|
|
398
|
-
status="
|
|
398
|
+
status="unknown",
|
|
399
399
|
severity="P2",
|
|
400
|
-
reason="
|
|
400
|
+
reason="Evolution is declared, but LaunchAgent inventory was not supplied",
|
|
401
401
|
launchagent_label=label,
|
|
402
402
|
)
|
|
403
403
|
labels = {str(item) for item in launchagent_labels}
|
|
404
404
|
if label in labels:
|
|
405
405
|
return EvolutionPolicyClassification(
|
|
406
|
-
status="
|
|
407
|
-
severity="
|
|
408
|
-
reason="
|
|
406
|
+
status="enabled_and_loaded",
|
|
407
|
+
severity="OK",
|
|
408
|
+
reason="Evolution is declared and loaded in the supplied inventory",
|
|
409
409
|
launchagent_label=label,
|
|
410
410
|
)
|
|
411
411
|
return EvolutionPolicyClassification(
|
|
412
|
-
status="
|
|
413
|
-
severity="
|
|
414
|
-
reason="
|
|
412
|
+
status="enabled_but_not_loaded",
|
|
413
|
+
severity="P1",
|
|
414
|
+
reason="Evolution is declared but absent from the supplied inventory",
|
|
415
415
|
launchagent_label=label,
|
|
416
416
|
)
|
|
417
417
|
|
|
@@ -17,7 +17,6 @@ _TOGGLEABLE_AUTOMATIONS = frozenset({
|
|
|
17
17
|
"morning-agent",
|
|
18
18
|
})
|
|
19
19
|
_EXCLUDED_HELPERS = frozenset({
|
|
20
|
-
"evolution",
|
|
21
20
|
"prevent-sleep",
|
|
22
21
|
"tcc-approve",
|
|
23
22
|
})
|
|
@@ -25,7 +24,9 @@ _NON_EDITABLE_REASONS: dict[str, str] = {
|
|
|
25
24
|
"catchup": "Runs only at login/wake catch-up; cadence is fixed by product design.",
|
|
26
25
|
"dashboard": "Persistent KeepAlive surface; cadence does not apply.",
|
|
27
26
|
}
|
|
28
|
-
_CLI_ONLY_REASONS: dict[str, str] = {
|
|
27
|
+
_CLI_ONLY_REASONS: dict[str, str] = {
|
|
28
|
+
"evolution": "Weekly support-ticket-only improvement cycle; adjust cadence from the CLI when needed.",
|
|
29
|
+
}
|
|
29
30
|
_INTERVAL_BOUNDS: dict[str, dict[str, int]] = {
|
|
30
31
|
"auto-close-sessions": {
|
|
31
32
|
"minimum_interval_seconds": 5 * 60,
|
package/src/crons/manifest.json
CHANGED
|
@@ -145,6 +145,21 @@
|
|
|
145
145
|
"run_on_boot": true,
|
|
146
146
|
"run_on_wake": true
|
|
147
147
|
},
|
|
148
|
+
{
|
|
149
|
+
"id": "evolution",
|
|
150
|
+
"script": "scripts/nexo-evolution-run.py",
|
|
151
|
+
"schedule_strategy": "machine_weekly_spread",
|
|
152
|
+
"schedule": {"hour": 5, "minute": 0, "weekday": 0},
|
|
153
|
+
"description": "Weekly self-improvement cycle — propose and evaluate changes",
|
|
154
|
+
"core": true,
|
|
155
|
+
"optional": "automation",
|
|
156
|
+
"recovery_policy": "catchup",
|
|
157
|
+
"idempotent": true,
|
|
158
|
+
"max_catchup_age": 1209600,
|
|
159
|
+
"stuck_after_seconds": 14400,
|
|
160
|
+
"run_on_boot": true,
|
|
161
|
+
"run_on_wake": true
|
|
162
|
+
},
|
|
148
163
|
{
|
|
149
164
|
"id": "followup-hygiene",
|
|
150
165
|
"script": "scripts/nexo-followup-hygiene.py",
|
|
@@ -1555,7 +1555,7 @@ class HeadlessEnforcer:
|
|
|
1555
1555
|
if "/.nexo/runtime/" in posix:
|
|
1556
1556
|
matches.append(path)
|
|
1557
1557
|
continue
|
|
1558
|
-
if
|
|
1558
|
+
if any(marker in posix for marker in ("/public_html/", "/htdocs/", "/wwwroot/")):
|
|
1559
1559
|
matches.append(path)
|
|
1560
1560
|
continue
|
|
1561
1561
|
if "/nexo-desktop/" in posix and (
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""NEXO Evolution Cycle — Self-improvement via Opus API.
|
|
2
|
+
|
|
3
|
+
Runs weekly after DMN. Analyzes patterns, proposes improvements.
|
|
4
|
+
v1: observe-only (all proposals logged as 'proposed' for the user to review).
|
|
5
|
+
v1.1 (future): sandbox execution of auto-approved changes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import paths
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sqlite3
|
|
14
|
+
import time
|
|
15
|
+
from datetime import datetime, date, timedelta
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from core_prompts import render_core_prompt
|
|
18
|
+
|
|
19
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
20
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(NEXO_HOME)))
|
|
21
|
+
NEXO_DB = paths.db_path()
|
|
22
|
+
# Evolution sandbox lives under the runtime root (equivalent to
|
|
23
|
+
# ``paths.runtime_dir() / "sandbox"``). Kept as ``NEXO_HOME / sandbox /
|
|
24
|
+
# workspace`` for backwards compatibility with existing installs that already
|
|
25
|
+
# have a populated sandbox at this path. Do NOT relocate without a migration.
|
|
26
|
+
SANDBOX_DIR = NEXO_HOME / "sandbox" / "workspace"
|
|
27
|
+
SNAPSHOTS_DIR = paths.snapshots_dir()
|
|
28
|
+
RESTORE_LOG = paths.logs_dir() / "snapshot-restores.log"
|
|
29
|
+
|
|
30
|
+
# Evolution config: brain/ (canonical) > cortex/ (legacy) > NEXO_CODE (dev)
|
|
31
|
+
def _resolve_evolution_file(name: str) -> Path:
|
|
32
|
+
for candidate in [paths.brain_dir() / name, NEXO_HOME / "cortex" / name, NEXO_CODE / name]:
|
|
33
|
+
if candidate.exists():
|
|
34
|
+
return candidate
|
|
35
|
+
return paths.brain_dir() / name # default canonical path
|
|
36
|
+
|
|
37
|
+
OBJECTIVE_FILE = _resolve_evolution_file("evolution-objective.json")
|
|
38
|
+
PROMPT_FILE = _resolve_evolution_file("evolution-prompt.md")
|
|
39
|
+
|
|
40
|
+
MAX_SNAPSHOTS = 8
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _normalize_dimensions(raw: dict | None) -> dict:
|
|
44
|
+
normalized = {}
|
|
45
|
+
for key, value in (raw or {}).items():
|
|
46
|
+
canonical_key = "agi" if key == "agi_readiness" else key
|
|
47
|
+
if isinstance(value, dict):
|
|
48
|
+
normalized[canonical_key] = {
|
|
49
|
+
"current": int(value.get("current", 0) or 0),
|
|
50
|
+
"target": int(value.get("target", 0) or 0),
|
|
51
|
+
}
|
|
52
|
+
else:
|
|
53
|
+
normalized[canonical_key] = {
|
|
54
|
+
"current": 0,
|
|
55
|
+
"target": int(value or 0),
|
|
56
|
+
}
|
|
57
|
+
return normalized
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def normalize_objective(obj: dict | None) -> dict:
|
|
61
|
+
"""Upgrade legacy objective files to the canonical schema."""
|
|
62
|
+
source = dict(obj or {})
|
|
63
|
+
|
|
64
|
+
if "evolution_mode" in source:
|
|
65
|
+
mode = str(source.get("evolution_mode") or "auto").strip().lower()
|
|
66
|
+
if mode in {"public", "public_core", "contributor", "draft_prs"}:
|
|
67
|
+
mode = "support_ticket"
|
|
68
|
+
else:
|
|
69
|
+
legacy_mode = str(source.get("review_mode") or "").strip().lower()
|
|
70
|
+
if legacy_mode in {"manual", "review"}:
|
|
71
|
+
mode = "review"
|
|
72
|
+
elif legacy_mode in {"managed", "hybrid", "owner", "core"}:
|
|
73
|
+
mode = "managed"
|
|
74
|
+
elif legacy_mode in {"public", "public_core", "contributor", "draft_prs"}:
|
|
75
|
+
mode = "support_ticket"
|
|
76
|
+
else:
|
|
77
|
+
mode = "auto"
|
|
78
|
+
|
|
79
|
+
if mode not in {"auto", "review", "managed", "public_core", "support_ticket"}:
|
|
80
|
+
mode = "auto"
|
|
81
|
+
|
|
82
|
+
dimensions = source.get("dimensions")
|
|
83
|
+
if not isinstance(dimensions, dict) or not dimensions:
|
|
84
|
+
dimensions = _normalize_dimensions(source.get("dimension_targets"))
|
|
85
|
+
else:
|
|
86
|
+
dimensions = _normalize_dimensions(dimensions)
|
|
87
|
+
|
|
88
|
+
defaults = {
|
|
89
|
+
"episodic_memory": {"current": 0, "target": 90},
|
|
90
|
+
"autonomy": {"current": 0, "target": 80},
|
|
91
|
+
"proactivity": {"current": 0, "target": 70},
|
|
92
|
+
"self_improvement": {"current": 0, "target": 60},
|
|
93
|
+
"agi": {"current": 0, "target": 20},
|
|
94
|
+
}
|
|
95
|
+
merged_dimensions = dict(defaults)
|
|
96
|
+
merged_dimensions.update(dimensions)
|
|
97
|
+
|
|
98
|
+
normalized = dict(source)
|
|
99
|
+
normalized["evolution_mode"] = mode
|
|
100
|
+
normalized["dimensions"] = merged_dimensions
|
|
101
|
+
normalized["total_evolutions"] = int(source.get("total_evolutions", source.get("cycles_completed", 0)) or 0)
|
|
102
|
+
normalized["last_evolution"] = source.get("last_evolution", source.get("last_cycle"))
|
|
103
|
+
normalized["total_proposals_made"] = int(source.get("total_proposals_made", 0) or 0)
|
|
104
|
+
normalized["total_auto_applied"] = int(source.get("total_auto_applied", 0) or 0)
|
|
105
|
+
normalized["consecutive_failures"] = int(source.get("consecutive_failures", 0) or 0)
|
|
106
|
+
normalized["history"] = source.get("history", []) if isinstance(source.get("history"), list) else []
|
|
107
|
+
normalized["evolution_enabled"] = bool(source.get("evolution_enabled", True))
|
|
108
|
+
normalized.pop("review_mode", None)
|
|
109
|
+
normalized.pop("dimension_targets", None)
|
|
110
|
+
normalized.pop("cycles_completed", None)
|
|
111
|
+
normalized.pop("last_cycle", None)
|
|
112
|
+
return normalized
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_objective() -> dict:
|
|
116
|
+
if OBJECTIVE_FILE.exists():
|
|
117
|
+
return normalize_objective(json.loads(OBJECTIVE_FILE.read_text()))
|
|
118
|
+
return normalize_objective({})
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def save_objective(obj: dict):
|
|
122
|
+
OBJECTIVE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
OBJECTIVE_FILE.write_text(json.dumps(normalize_objective(obj), indent=2, ensure_ascii=False))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_week_data(db_path: str) -> dict:
|
|
127
|
+
"""Gather last 7 days of learnings, decisions, changes, diaries."""
|
|
128
|
+
conn = sqlite3.connect(db_path, timeout=10)
|
|
129
|
+
try:
|
|
130
|
+
conn.row_factory = sqlite3.Row
|
|
131
|
+
cutoff_epoch = time.time() - 7 * 86400
|
|
132
|
+
cutoff_date = (date.today() - timedelta(days=7)).isoformat()
|
|
133
|
+
|
|
134
|
+
data = {}
|
|
135
|
+
|
|
136
|
+
rows = conn.execute(
|
|
137
|
+
"SELECT category, title, content FROM learnings WHERE created_at > ? ORDER BY created_at DESC LIMIT 50",
|
|
138
|
+
(cutoff_epoch,)
|
|
139
|
+
).fetchall()
|
|
140
|
+
data["learnings"] = [dict(r) for r in rows]
|
|
141
|
+
|
|
142
|
+
rows = conn.execute(
|
|
143
|
+
"SELECT domain, decision, alternatives, based_on, confidence, outcome FROM decisions "
|
|
144
|
+
"WHERE created_at > ? ORDER BY created_at DESC LIMIT 20",
|
|
145
|
+
(cutoff_date,)
|
|
146
|
+
).fetchall()
|
|
147
|
+
data["decisions"] = [dict(r) for r in rows]
|
|
148
|
+
|
|
149
|
+
rows = conn.execute(
|
|
150
|
+
"SELECT files, what_changed, why, affects, risks FROM change_log "
|
|
151
|
+
"WHERE created_at > ? ORDER BY created_at DESC LIMIT 30",
|
|
152
|
+
(cutoff_date,)
|
|
153
|
+
).fetchall()
|
|
154
|
+
data["changes"] = [dict(r) for r in rows]
|
|
155
|
+
|
|
156
|
+
rows = conn.execute(
|
|
157
|
+
"SELECT summary, decisions as diary_decisions, pending, mental_state, domain, user_signals "
|
|
158
|
+
"FROM session_diary WHERE created_at > ? ORDER BY created_at DESC LIMIT 20",
|
|
159
|
+
(cutoff_date,)
|
|
160
|
+
).fetchall()
|
|
161
|
+
data["diaries"] = [dict(r) for r in rows]
|
|
162
|
+
|
|
163
|
+
rows = conn.execute(
|
|
164
|
+
"SELECT * FROM evolution_log ORDER BY id DESC LIMIT 20"
|
|
165
|
+
).fetchall()
|
|
166
|
+
data["evolution_history"] = [dict(r) for r in rows]
|
|
167
|
+
|
|
168
|
+
rows = conn.execute(
|
|
169
|
+
"SELECT dimension, score, delta, measured_at FROM evolution_metrics "
|
|
170
|
+
"WHERE id IN (SELECT MAX(id) FROM evolution_metrics GROUP BY dimension)"
|
|
171
|
+
).fetchall()
|
|
172
|
+
data["current_metrics"] = {r["dimension"]: dict(r) for r in rows}
|
|
173
|
+
|
|
174
|
+
return data
|
|
175
|
+
finally:
|
|
176
|
+
conn.close()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def create_snapshot(files_to_backup: list) -> str:
|
|
180
|
+
"""Create a snapshot of specific files before modification."""
|
|
181
|
+
ts = datetime.now().strftime("%Y-%m-%dT%H:%M")
|
|
182
|
+
snap_dir = SNAPSHOTS_DIR / ts
|
|
183
|
+
files_dir = snap_dir / "files"
|
|
184
|
+
|
|
185
|
+
manifest = {
|
|
186
|
+
"created_at": datetime.now().isoformat(),
|
|
187
|
+
"files": [],
|
|
188
|
+
"reason": "evolution_cycle"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for filepath in files_to_backup:
|
|
192
|
+
fp = Path(filepath).expanduser()
|
|
193
|
+
if fp.exists():
|
|
194
|
+
rel = str(fp).replace(str(Path.home()) + "/", "")
|
|
195
|
+
dest = files_dir / rel
|
|
196
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
if os.path.abspath(str(fp)) == os.path.abspath(str(dest)):
|
|
198
|
+
continue # Skip: source and destination are the same file
|
|
199
|
+
shutil.copy2(fp, dest)
|
|
200
|
+
manifest["files"].append(rel)
|
|
201
|
+
|
|
202
|
+
snap_dir.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
(snap_dir / "manifest.json").write_text(json.dumps(manifest, indent=2))
|
|
204
|
+
|
|
205
|
+
latest = SNAPSHOTS_DIR / "latest"
|
|
206
|
+
if latest.is_symlink():
|
|
207
|
+
latest.unlink()
|
|
208
|
+
latest.symlink_to(snap_dir)
|
|
209
|
+
|
|
210
|
+
_cleanup_snapshots()
|
|
211
|
+
return str(snap_dir)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _cleanup_snapshots():
|
|
215
|
+
"""Remove old snapshots, keeping MAX_SNAPSHOTS most recent + golden."""
|
|
216
|
+
if not SNAPSHOTS_DIR.exists():
|
|
217
|
+
return
|
|
218
|
+
snaps = sorted(
|
|
219
|
+
[d for d in SNAPSHOTS_DIR.iterdir()
|
|
220
|
+
if d.is_dir() and d.name not in ("latest", "golden")],
|
|
221
|
+
key=lambda d: d.stat().st_mtime,
|
|
222
|
+
reverse=True
|
|
223
|
+
)
|
|
224
|
+
for old in snaps[MAX_SNAPSHOTS:]:
|
|
225
|
+
shutil.rmtree(old)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def dry_run_restore_test() -> bool:
|
|
229
|
+
"""Test that snapshot+restore works before making real changes."""
|
|
230
|
+
test_file = SANDBOX_DIR / "restore-test.txt"
|
|
231
|
+
test_file.parent.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
test_file.write_text("original_content")
|
|
233
|
+
|
|
234
|
+
snap_dir = create_snapshot([str(test_file)])
|
|
235
|
+
|
|
236
|
+
test_file.write_text("modified_content")
|
|
237
|
+
|
|
238
|
+
# Find restore script: repo scripts/ first, then installed core/scripts/.
|
|
239
|
+
_nexo_code = Path(os.environ.get("NEXO_CODE", ""))
|
|
240
|
+
restore_script = None
|
|
241
|
+
for candidate in [_nexo_code / "scripts" / "nexo-snapshot-restore.sh",
|
|
242
|
+
paths.core_scripts_dir() / "nexo-snapshot-restore.sh"]:
|
|
243
|
+
if candidate.exists():
|
|
244
|
+
restore_script = candidate
|
|
245
|
+
break
|
|
246
|
+
if not restore_script:
|
|
247
|
+
test_file.unlink(missing_ok=True)
|
|
248
|
+
return False # No restore script available
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
subprocess.run(
|
|
252
|
+
[str(restore_script), snap_dir],
|
|
253
|
+
capture_output=True, timeout=10, check=True
|
|
254
|
+
)
|
|
255
|
+
content = test_file.read_text()
|
|
256
|
+
test_file.unlink(missing_ok=True)
|
|
257
|
+
# Clean up test snapshot
|
|
258
|
+
snap_path = Path(snap_dir)
|
|
259
|
+
if snap_path.exists():
|
|
260
|
+
shutil.rmtree(snap_path)
|
|
261
|
+
return content == "original_content"
|
|
262
|
+
except Exception:
|
|
263
|
+
test_file.unlink(missing_ok=True)
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def build_evolution_prompt(week_data: dict, objective: dict) -> str:
|
|
268
|
+
"""Build a SHORT prompt — CLI investigates on its own using tools."""
|
|
269
|
+
|
|
270
|
+
objective_dims = normalize_objective(objective).get("dimensions", {})
|
|
271
|
+
current_scores = {
|
|
272
|
+
dim: int(m["score"])
|
|
273
|
+
for dim, m in week_data.get("current_metrics", {}).items()
|
|
274
|
+
if isinstance(m, dict) and isinstance(m.get("score"), (int, float))
|
|
275
|
+
}
|
|
276
|
+
if not current_scores:
|
|
277
|
+
current_scores = {
|
|
278
|
+
dim: int((payload or {}).get("current", 0) or 0)
|
|
279
|
+
for dim, payload in objective_dims.items()
|
|
280
|
+
if isinstance(payload, dict)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
# Summary stats only — CLI will dig deeper with tools
|
|
284
|
+
stats = {
|
|
285
|
+
"learnings_this_week": len(week_data.get("learnings", [])),
|
|
286
|
+
"decisions_this_week": len(week_data.get("decisions", [])),
|
|
287
|
+
"changes_this_week": len(week_data.get("changes", [])),
|
|
288
|
+
"diaries_this_week": len(week_data.get("diaries", [])),
|
|
289
|
+
"evolution_history": len(week_data.get("evolution_history", [])),
|
|
290
|
+
"current_scores": current_scores,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
mode = normalize_objective(objective).get("evolution_mode", "auto")
|
|
294
|
+
total = objective.get("total_evolutions", 0)
|
|
295
|
+
max_auto = max_auto_changes(total)
|
|
296
|
+
if mode == "review":
|
|
297
|
+
mode_desc = "review-only, nothing executes automatically"
|
|
298
|
+
safe_zones = "~/.nexo/personal/scripts/, ~/.nexo/personal/plugins/, ~/.nexo/personal/brain/"
|
|
299
|
+
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, cognitive.py, knowledge_graph.py, tools_*.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md"
|
|
300
|
+
elif mode == "managed":
|
|
301
|
+
mode_desc = f"owner-managed, max {max_auto} auto-applied changes with rollback and followups"
|
|
302
|
+
safe_zones = "~/.nexo/personal/scripts/, ~/.nexo/personal/plugins/, ~/.nexo/personal/brain/, NEXO_CODE/src, repo bin/docs/templates/tests"
|
|
303
|
+
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md, personality.md, user-profile.md"
|
|
304
|
+
elif mode in {"public_core", "support_ticket"}:
|
|
305
|
+
mode_desc = "support-ticket mode, no automatic code writes and no GitHub publishing"
|
|
306
|
+
safe_zones = "read-only analysis plus anonymized support-ticket creation"
|
|
307
|
+
immutable_files = "all local runtime data, personal files, local DBs/logs, prompts, secrets, CLAUDE.md, AGENTS.md, user-profile.md"
|
|
308
|
+
else:
|
|
309
|
+
mode_desc = f"public auto, max {max_auto} auto-applied changes in personal safe zones"
|
|
310
|
+
safe_zones = "~/.nexo/personal/scripts/, ~/.nexo/personal/plugins/"
|
|
311
|
+
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, cognitive.py, knowledge_graph.py, tools_*.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md"
|
|
312
|
+
|
|
313
|
+
return render_core_prompt(
|
|
314
|
+
"evolution-weekly",
|
|
315
|
+
learnings_this_week=stats["learnings_this_week"],
|
|
316
|
+
decisions_this_week=stats["decisions_this_week"],
|
|
317
|
+
changes_this_week=stats["changes_this_week"],
|
|
318
|
+
diaries_this_week=stats["diaries_this_week"],
|
|
319
|
+
evolution_history=stats["evolution_history"],
|
|
320
|
+
current_scores_json=json.dumps(stats["current_scores"]),
|
|
321
|
+
mode=mode,
|
|
322
|
+
mode_desc=mode_desc,
|
|
323
|
+
cycle_number=total + 1,
|
|
324
|
+
nexo_db=NEXO_DB,
|
|
325
|
+
week_cutoff_ts=time.time() - 7 * 86400,
|
|
326
|
+
safe_zones=safe_zones,
|
|
327
|
+
immutable_files=immutable_files,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def build_public_contribution_prompt(
|
|
332
|
+
*,
|
|
333
|
+
repo_root: str,
|
|
334
|
+
cycle_number: int,
|
|
335
|
+
queued_candidate: dict | None = None,
|
|
336
|
+
) -> str:
|
|
337
|
+
"""Prompt for the public-core contributor mode.
|
|
338
|
+
|
|
339
|
+
This prompt must never rely on private runtime state. It should inspect only
|
|
340
|
+
the isolated public repo checkout, make one coherent improvement, and end
|
|
341
|
+
by returning machine-readable summary JSON.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
queued_section = ""
|
|
345
|
+
if queued_candidate:
|
|
346
|
+
queued_files = "\n".join(
|
|
347
|
+
f"- {path}" for path in (queued_candidate.get("files_changed") or [])[:20]
|
|
348
|
+
) or "- (no files recorded)"
|
|
349
|
+
queued_source = str((queued_candidate.get("metadata") or {}).get("source") or "managed-runtime")
|
|
350
|
+
queued_section = f"""
|
|
351
|
+
|
|
352
|
+
PRIORITY PUBLIC-PORT QUEUE ITEM:
|
|
353
|
+
- Source: {queued_source}
|
|
354
|
+
- Title: {str(queued_candidate.get("title") or "").strip()}
|
|
355
|
+
- Why it matters: {str(queued_candidate.get("reasoning") or "").strip()}
|
|
356
|
+
- Files originally touched:
|
|
357
|
+
{queued_files}
|
|
358
|
+
|
|
359
|
+
This item was already fixed or detected outside the public contribution runner.
|
|
360
|
+
Before inventing another improvement, verify whether the public repository still
|
|
361
|
+
needs the same change and port it if necessary. If the repo is already correct,
|
|
362
|
+
make the smallest validating change that captures the same gap.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
return render_core_prompt(
|
|
366
|
+
"evolution-public-contribution",
|
|
367
|
+
repo_root=repo_root,
|
|
368
|
+
cycle_number=cycle_number,
|
|
369
|
+
queued_section=queued_section,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def build_public_pr_review_prompt(
|
|
374
|
+
*,
|
|
375
|
+
pr_number: int,
|
|
376
|
+
title: str,
|
|
377
|
+
author: str,
|
|
378
|
+
url: str,
|
|
379
|
+
body: str,
|
|
380
|
+
files: list[str],
|
|
381
|
+
diff_text: str,
|
|
382
|
+
) -> str:
|
|
383
|
+
"""Legacy prompt template kept for old artifacts; active Evolution no longer reviews PRs."""
|
|
384
|
+
|
|
385
|
+
rendered_files = "\n".join(f"- {path}" for path in files[:40]) if files else "- (no file list provided)"
|
|
386
|
+
trimmed_diff = (diff_text or "").strip()
|
|
387
|
+
if len(trimmed_diff) > 80000:
|
|
388
|
+
trimmed_diff = trimmed_diff[:80000] + "\n\n[diff truncated by NEXO]"
|
|
389
|
+
|
|
390
|
+
return render_core_prompt(
|
|
391
|
+
"evolution-public-pr-review",
|
|
392
|
+
pr_number=pr_number,
|
|
393
|
+
author=author,
|
|
394
|
+
url=url,
|
|
395
|
+
title=title,
|
|
396
|
+
body=body or "(empty)",
|
|
397
|
+
rendered_files=rendered_files,
|
|
398
|
+
trimmed_diff=trimmed_diff or "(empty diff)",
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def max_auto_changes(total_evolutions: int) -> int:
|
|
403
|
+
"""Progressive trust: 1 for first 4 cycles, 2 for next 4, then 3."""
|
|
404
|
+
if total_evolutions < 4:
|
|
405
|
+
return 1
|
|
406
|
+
elif total_evolutions < 8:
|
|
407
|
+
return 2
|
|
408
|
+
return 3
|