nexo-brain 2.5.0 → 2.5.1
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 +15 -2
- package/bin/nexo-brain.js +30 -1
- package/package.json +7 -2
- package/src/auto_update.py +35 -1
- package/src/cli.py +116 -2
- package/src/evolution_cycle.py +86 -6
- package/src/plugins/evolution.py +9 -2
- package/src/scripts/nexo-evolution-run.py +141 -55
package/README.md
CHANGED
|
@@ -535,8 +535,12 @@ That's it. No need to run `claude` manually. Your operator will greet you immedi
|
|
|
535
535
|
| MCP server | 147+ tools for memory, cognition, learning, guard | NEXO_HOME/ |
|
|
536
536
|
| Plugins | Guard, episodic memory, cognitive memory, entities, preferences, update, etc. | Code: src/plugins/, Personal: NEXO_HOME/plugins/ |
|
|
537
537
|
| Hooks (7) | SessionStart, Stop, PostToolUse, PreCompact, PostCompact | NEXO_HOME/hooks/ |
|
|
538
|
-
| Nervous system |
|
|
539
|
-
| Dashboard | Web UI at localhost:6174 (
|
|
538
|
+
| Nervous system | 17 autonomous processes (decay, sleep, audit, evolution, watchdog, orchestrator, dashboard, etc.) | NEXO_HOME/scripts/ |
|
|
539
|
+
| Dashboard | Web UI at localhost:6174 (23 modules, dark theme) — opt-in, always-on | NEXO_HOME/dashboard/ |
|
|
540
|
+
| Runtime CLI | `nexo` command: scripts, doctor, skills, update | NEXO_HOME/bin/ |
|
|
541
|
+
| Doctor | Unified diagnostics: boot/runtime/deep tiers, `--fix` mode | src/doctor/ |
|
|
542
|
+
| Skills v2 | Executable skills with guide/execute/hybrid modes, approval levels | NEXO_HOME/skills/ |
|
|
543
|
+
| Day Orchestrator | Autonomous cycles every 15 min (8:00-23:00) — opt-in | LaunchAgent |
|
|
540
544
|
| CLAUDE.md | Complete operator instructions (Codex, hooks, guard, trust, memory) | ~/.claude/CLAUDE.md |
|
|
541
545
|
| Schedule config | schedule.json with customizable process times and timezone | NEXO_HOME/config/ |
|
|
542
546
|
| Auto-update | Non-blocking startup check (5s max), opt-out via schedule.json | Built into server startup |
|
|
@@ -789,6 +793,15 @@ If NEXO Brain is useful to you, consider:
|
|
|
789
793
|
|
|
790
794
|
## Changelog
|
|
791
795
|
|
|
796
|
+
### v2.5.0 — Runtime CLI, Doctor, Skills v2, Day Orchestrator (2026-04-03)
|
|
797
|
+
- **Runtime CLI** (`nexo`): New operational CLI separate from installer. `nexo scripts list/run/doctor/call` for personal scripts, `nexo doctor` for diagnostics, `nexo skills apply` for executable skills, `nexo update` for one-step sync.
|
|
798
|
+
- **Unified Doctor**: Modular diagnostic system with boot/runtime/deep tiers. Report-only by default, deterministic `--fix` mode. MCP tool `nexo_doctor`. LaunchAgent schedule drift detection and reconciliation.
|
|
799
|
+
- **Skills v2**: Executable skills with guide/execute/hybrid modes. Security levels (read-only/local/remote) with explicit approval. Core vs personal vs community directories. Deep Sleep auto-evolution integration.
|
|
800
|
+
- **Day Orchestrator**: Autonomous NEXO cycles every 15 min (8:00-23:00). Launches Claude Code headless with full MCP. Checks followups, emails, infra — acts autonomously, emails user only when needed. Opt-in.
|
|
801
|
+
- **Dashboard always-on**: Web UI at localhost:6174 as persistent LaunchAgent. 23 modules, Jinja2 templating, dark theme. Opt-in.
|
|
802
|
+
- **Personal Scripts Framework**: Auto-discovery in NEXO_HOME/scripts/, inline metadata, runtime detection, forbidden-pattern validation, vendorable helper, template.
|
|
803
|
+
- Configurable operator name (UserContext singleton), watchdog normalized to 30 min, LaunchAgent drift fix.
|
|
804
|
+
|
|
792
805
|
### v2.4.0 — Skills, Cron Scheduler, Security, Full Audit (2026-04-03)
|
|
793
806
|
- **Skill Auto-Creation**: Deep Sleep extracts reusable procedures from sessions. Content stored as markdown with steps and gotchas. Trust pipeline with autonomous quality control.
|
|
794
807
|
- **Cron Scheduler**: execution tracking (`cron_runs` table), `nexo_schedule_status` and `nexo_schedule_add` MCP tools, universal cron wrapper for all processes.
|
package/bin/nexo-brain.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const { execSync, spawnSync } = require("child_process");
|
|
18
|
+
const crypto = require("crypto");
|
|
18
19
|
const fs = require("fs");
|
|
19
20
|
const path = require("path");
|
|
20
21
|
const readline = require("readline");
|
|
@@ -59,6 +60,33 @@ function log(msg) {
|
|
|
59
60
|
console.log(` ${msg}`);
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function syncWatchdogHashRegistry(nexoHome) {
|
|
64
|
+
try {
|
|
65
|
+
const watchdogPath = path.join(nexoHome, "scripts", "nexo-watchdog.sh");
|
|
66
|
+
if (!fs.existsSync(watchdogPath)) return;
|
|
67
|
+
|
|
68
|
+
const registryPath = path.join(nexoHome, "scripts", ".watchdog-hashes");
|
|
69
|
+
const entries = new Map();
|
|
70
|
+
if (fs.existsSync(registryPath)) {
|
|
71
|
+
for (const line of fs.readFileSync(registryPath, "utf8").split(/\r?\n/)) {
|
|
72
|
+
if (!line.includes("|")) continue;
|
|
73
|
+
const [filePath, expectedHash] = line.split("|");
|
|
74
|
+
if (filePath) entries.set(filePath, expectedHash || "");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const digest = crypto.createHash("sha256").update(fs.readFileSync(watchdogPath)).digest("hex");
|
|
79
|
+
entries.set(watchdogPath, digest);
|
|
80
|
+
const body = Array.from(entries.entries())
|
|
81
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
82
|
+
.map(([filePath, hash]) => `${filePath}|${hash}`)
|
|
83
|
+
.join("\n");
|
|
84
|
+
fs.writeFileSync(registryPath, `${body}\n`);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
log(`WARN: could not sync watchdog hash registry: ${err.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
63
91
|
// CORE PROCESS & HOOK DEFINITIONS
|
|
64
92
|
// All 13 nightly/periodic processes and all 8 core hooks that make NEXO functional.
|
|
@@ -1370,7 +1398,7 @@ async function main() {
|
|
|
1370
1398
|
objective: "Improve operational excellence and reduce repeated errors",
|
|
1371
1399
|
focus_areas: ["error_prevention", "proactivity", "memory_quality"],
|
|
1372
1400
|
evolution_enabled: true,
|
|
1373
|
-
evolution_mode: "
|
|
1401
|
+
evolution_mode: "auto",
|
|
1374
1402
|
dimensions: {
|
|
1375
1403
|
episodic_memory: { current: 0, target: 90 },
|
|
1376
1404
|
autonomy: { current: 0, target: 80 },
|
|
@@ -1502,6 +1530,7 @@ async function main() {
|
|
|
1502
1530
|
fs.readdirSync(scriptsDest).filter(f => f.endsWith(".sh")).forEach(f => {
|
|
1503
1531
|
fs.chmodSync(path.join(scriptsDest, f), "755");
|
|
1504
1532
|
});
|
|
1533
|
+
syncWatchdogHashRegistry(NEXO_HOME);
|
|
1505
1534
|
}
|
|
1506
1535
|
|
|
1507
1536
|
// Core skills are shipped separately from personal skills.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — Cognitive co-operator for Claude Code. Memory, emotional intelligence, overnight learning (Deep Sleep), cron management, trust scoring, and adaptive calibration.",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,12 @@
|
|
|
39
39
|
"cron-manifest",
|
|
40
40
|
"session-tone",
|
|
41
41
|
"mood-tracking",
|
|
42
|
-
"productivity-analysis"
|
|
42
|
+
"productivity-analysis",
|
|
43
|
+
"runtime-cli",
|
|
44
|
+
"doctor",
|
|
45
|
+
"executable-skills",
|
|
46
|
+
"day-orchestrator",
|
|
47
|
+
"work-continuity"
|
|
43
48
|
],
|
|
44
49
|
"author": "NEXO Brain <info@nexo-brain.com>",
|
|
45
50
|
"license": "AGPL-3.0",
|
package/src/auto_update.py
CHANGED
|
@@ -8,6 +8,7 @@ This is separate from plugins/update.py which handles MANUAL updates with rollba
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import hashlib
|
|
11
12
|
import os
|
|
12
13
|
import re
|
|
13
14
|
import subprocess
|
|
@@ -56,6 +57,30 @@ def _write_last_check(data: dict):
|
|
|
56
57
|
_log(f"Failed to write last-check file: {e}")
|
|
57
58
|
|
|
58
59
|
|
|
60
|
+
def _sync_watchdog_hash_registry():
|
|
61
|
+
"""Keep the immutable-hash registry aligned with the installed watchdog script."""
|
|
62
|
+
try:
|
|
63
|
+
watchdog_file = NEXO_HOME / "scripts" / "nexo-watchdog.sh"
|
|
64
|
+
if not watchdog_file.exists():
|
|
65
|
+
return
|
|
66
|
+
registry_file = NEXO_HOME / "scripts" / ".watchdog-hashes"
|
|
67
|
+
entries: dict[str, str] = {}
|
|
68
|
+
if registry_file.exists():
|
|
69
|
+
for line in registry_file.read_text().splitlines():
|
|
70
|
+
if "|" not in line:
|
|
71
|
+
continue
|
|
72
|
+
filepath, expected = line.split("|", 1)
|
|
73
|
+
if filepath:
|
|
74
|
+
entries[filepath] = expected
|
|
75
|
+
actual_hash = hashlib.sha256(watchdog_file.read_bytes()).hexdigest()
|
|
76
|
+
entries[str(watchdog_file)] = actual_hash
|
|
77
|
+
registry_file.write_text(
|
|
78
|
+
"\n".join(f"{filepath}|{digest}" for filepath, digest in sorted(entries.items())) + "\n"
|
|
79
|
+
)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
_log(f"watchdog hash registry sync error: {e}")
|
|
82
|
+
|
|
83
|
+
|
|
59
84
|
def _is_git_repo() -> bool:
|
|
60
85
|
"""Check if REPO_DIR is inside a git repository."""
|
|
61
86
|
try:
|
|
@@ -811,13 +836,14 @@ def auto_update_check() -> dict:
|
|
|
811
836
|
# Backfill evolution-objective.json for existing installs
|
|
812
837
|
try:
|
|
813
838
|
evo_obj_path = NEXO_HOME / "brain" / "evolution-objective.json"
|
|
839
|
+
from evolution_cycle import normalize_objective
|
|
814
840
|
if not evo_obj_path.exists():
|
|
815
841
|
(NEXO_HOME / "brain").mkdir(parents=True, exist_ok=True)
|
|
816
842
|
default_objective = {
|
|
817
843
|
"objective": "Improve operational excellence and reduce repeated errors",
|
|
818
844
|
"focus_areas": ["error_prevention", "proactivity", "memory_quality"],
|
|
819
845
|
"evolution_enabled": True,
|
|
820
|
-
"evolution_mode": "
|
|
846
|
+
"evolution_mode": "auto",
|
|
821
847
|
"dimensions": {
|
|
822
848
|
"episodic_memory": {"current": 0, "target": 90},
|
|
823
849
|
"autonomy": {"current": 0, "target": 80},
|
|
@@ -831,6 +857,12 @@ def auto_update_check() -> dict:
|
|
|
831
857
|
}
|
|
832
858
|
evo_obj_path.write_text(json.dumps(default_objective, indent=2))
|
|
833
859
|
_log("Backfilled evolution-objective.json for existing install")
|
|
860
|
+
else:
|
|
861
|
+
raw_objective = json.loads(evo_obj_path.read_text())
|
|
862
|
+
normalized = normalize_objective(raw_objective)
|
|
863
|
+
if normalized != raw_objective:
|
|
864
|
+
evo_obj_path.write_text(json.dumps(normalized, indent=2, ensure_ascii=False))
|
|
865
|
+
_log("Normalized legacy evolution-objective.json")
|
|
834
866
|
except Exception as e:
|
|
835
867
|
_log(f"evolution-objective.json backfill error: {e}")
|
|
836
868
|
|
|
@@ -853,6 +885,8 @@ def auto_update_check() -> dict:
|
|
|
853
885
|
except Exception as e:
|
|
854
886
|
_log(f"scripts backfill error: {e}")
|
|
855
887
|
|
|
888
|
+
_sync_watchdog_hash_registry()
|
|
889
|
+
|
|
856
890
|
# Backfill runtime CLI modules for existing installs
|
|
857
891
|
try:
|
|
858
892
|
for fname in ("cli.py", "script_registry.py", "skills_runtime.py"):
|
package/src/cli.py
CHANGED
|
@@ -30,6 +30,17 @@ from pathlib import Path
|
|
|
30
30
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
31
31
|
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
|
|
32
32
|
|
|
33
|
+
|
|
34
|
+
def _get_version() -> str:
|
|
35
|
+
"""Read version from package.json automatically."""
|
|
36
|
+
for candidate in [NEXO_CODE.parent / "package.json", NEXO_HOME / "package.json"]:
|
|
37
|
+
try:
|
|
38
|
+
if candidate.is_file():
|
|
39
|
+
return json.loads(candidate.read_text()).get("version", "?")
|
|
40
|
+
except Exception:
|
|
41
|
+
continue
|
|
42
|
+
return "?"
|
|
43
|
+
|
|
33
44
|
# Ensure src/ is on path for imports
|
|
34
45
|
if str(NEXO_CODE) not in sys.path:
|
|
35
46
|
sys.path.insert(0, str(NEXO_CODE))
|
|
@@ -357,6 +368,71 @@ def _update(args):
|
|
|
357
368
|
return 0
|
|
358
369
|
|
|
359
370
|
|
|
371
|
+
def _service_control(service_name: str, action: str) -> int:
|
|
372
|
+
"""Control a LaunchAgent/systemd service: on, off, status."""
|
|
373
|
+
import platform as plat
|
|
374
|
+
|
|
375
|
+
label = f"com.nexo.{service_name}"
|
|
376
|
+
|
|
377
|
+
if plat.system() != "Darwin":
|
|
378
|
+
print(f"Service control only supported on macOS for now.", file=sys.stderr)
|
|
379
|
+
return 1
|
|
380
|
+
|
|
381
|
+
plist_path = Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
|
|
382
|
+
uid = os.getuid()
|
|
383
|
+
|
|
384
|
+
if action == "status":
|
|
385
|
+
result = subprocess.run(
|
|
386
|
+
["launchctl", "list"],
|
|
387
|
+
capture_output=True, text=True,
|
|
388
|
+
)
|
|
389
|
+
running = label in (result.stdout or "")
|
|
390
|
+
if running:
|
|
391
|
+
print(f"{service_name}: running")
|
|
392
|
+
else:
|
|
393
|
+
print(f"{service_name}: stopped")
|
|
394
|
+
return 0
|
|
395
|
+
|
|
396
|
+
if action == "on":
|
|
397
|
+
if not plist_path.is_file():
|
|
398
|
+
print(f"LaunchAgent not found: {plist_path}", file=sys.stderr)
|
|
399
|
+
print(f"Run 'nexo-brain' to install it, or enable it during setup.", file=sys.stderr)
|
|
400
|
+
return 1
|
|
401
|
+
subprocess.run(
|
|
402
|
+
["launchctl", "bootout", f"gui/{uid}", str(plist_path)],
|
|
403
|
+
capture_output=True,
|
|
404
|
+
)
|
|
405
|
+
result = subprocess.run(
|
|
406
|
+
["launchctl", "bootstrap", f"gui/{uid}", str(plist_path)],
|
|
407
|
+
capture_output=True, text=True,
|
|
408
|
+
)
|
|
409
|
+
if result.returncode == 0:
|
|
410
|
+
print(f"{service_name}: started")
|
|
411
|
+
else:
|
|
412
|
+
print(f"Failed to start {service_name}: {result.stderr.strip()}", file=sys.stderr)
|
|
413
|
+
return 1
|
|
414
|
+
return 0
|
|
415
|
+
|
|
416
|
+
if action == "off":
|
|
417
|
+
result = subprocess.run(
|
|
418
|
+
["launchctl", "bootout", f"gui/{uid}", str(plist_path)],
|
|
419
|
+
capture_output=True, text=True,
|
|
420
|
+
)
|
|
421
|
+
print(f"{service_name}: stopped")
|
|
422
|
+
return 0
|
|
423
|
+
|
|
424
|
+
print(f"Unknown action: {action}. Use on, off, or status.", file=sys.stderr)
|
|
425
|
+
return 1
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _dashboard(args):
|
|
429
|
+
return _service_control("dashboard", args.action)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _orchestrator(args):
|
|
433
|
+
return _service_control("day-orchestrator", args.action)
|
|
434
|
+
|
|
435
|
+
|
|
360
436
|
def _doctor(args):
|
|
361
437
|
"""Run unified doctor diagnostics."""
|
|
362
438
|
try:
|
|
@@ -482,8 +558,27 @@ def _skills_evolution(args):
|
|
|
482
558
|
return 0
|
|
483
559
|
|
|
484
560
|
|
|
561
|
+
def _print_help():
|
|
562
|
+
v = _get_version()
|
|
563
|
+
print(f"""NEXO Runtime CLI v{v}
|
|
564
|
+
|
|
565
|
+
Commands:
|
|
566
|
+
nexo doctor [--tier boot|runtime|deep|all] [--fix] System diagnostics
|
|
567
|
+
nexo scripts list|run|doctor|call Personal scripts
|
|
568
|
+
nexo skills list|apply|sync|approve Executable skills
|
|
569
|
+
nexo update Sync repo to NEXO_HOME
|
|
570
|
+
nexo dashboard on|off|status Web dashboard control
|
|
571
|
+
nexo orchestrator on|off|status Autonomous mode control
|
|
572
|
+
|
|
573
|
+
Run 'nexo <command> --help' for details.
|
|
574
|
+
Homepage: https://nexo-brain.com
|
|
575
|
+
GitHub: https://github.com/wazionapps/nexo""")
|
|
576
|
+
|
|
577
|
+
|
|
485
578
|
def main():
|
|
486
|
-
parser = argparse.ArgumentParser(prog="nexo", description="NEXO Runtime CLI")
|
|
579
|
+
parser = argparse.ArgumentParser(prog="nexo", description="NEXO Runtime CLI", add_help=False)
|
|
580
|
+
parser.add_argument("-h", "--help", action="store_true", help="Show help")
|
|
581
|
+
parser.add_argument("-v", "--version", action="store_true", help="Show version")
|
|
487
582
|
sub = parser.add_subparsers(dest="command")
|
|
488
583
|
|
|
489
584
|
# -- scripts --
|
|
@@ -560,8 +655,23 @@ def main():
|
|
|
560
655
|
skills_evolution_p = skills_sub.add_parser("evolution", help="Evolution candidates")
|
|
561
656
|
skills_evolution_p.add_argument("--json", action="store_true", help="JSON output")
|
|
562
657
|
|
|
658
|
+
# -- dashboard --
|
|
659
|
+
dashboard_parser = sub.add_parser("dashboard", help="Web dashboard control")
|
|
660
|
+
dashboard_parser.add_argument("action", choices=["on", "off", "status"], help="Start, stop, or check dashboard")
|
|
661
|
+
|
|
662
|
+
# -- orchestrator --
|
|
663
|
+
orchestrator_parser = sub.add_parser("orchestrator", help="Autonomous mode control")
|
|
664
|
+
orchestrator_parser.add_argument("action", choices=["on", "off", "status"], help="Start, stop, or check orchestrator")
|
|
665
|
+
|
|
563
666
|
args = parser.parse_args()
|
|
564
667
|
|
|
668
|
+
if args.help or (not args.command and not args.version):
|
|
669
|
+
_print_help()
|
|
670
|
+
return 0
|
|
671
|
+
if args.version:
|
|
672
|
+
print(f"nexo v{_get_version()}")
|
|
673
|
+
return 0
|
|
674
|
+
|
|
565
675
|
if args.command == "scripts":
|
|
566
676
|
if args.scripts_command == "list":
|
|
567
677
|
return _scripts_list(args)
|
|
@@ -596,8 +706,12 @@ def main():
|
|
|
596
706
|
else:
|
|
597
707
|
skills_parser.print_help()
|
|
598
708
|
return 0
|
|
709
|
+
elif args.command == "dashboard":
|
|
710
|
+
return _dashboard(args)
|
|
711
|
+
elif args.command == "orchestrator":
|
|
712
|
+
return _orchestrator(args)
|
|
599
713
|
else:
|
|
600
|
-
|
|
714
|
+
_print_help()
|
|
601
715
|
return 0
|
|
602
716
|
|
|
603
717
|
|
package/src/evolution_cycle.py
CHANGED
|
@@ -34,14 +34,83 @@ PROMPT_FILE = _resolve_evolution_file("evolution-prompt.md")
|
|
|
34
34
|
MAX_SNAPSHOTS = 8
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _normalize_dimensions(raw: dict | None) -> dict:
|
|
38
|
+
normalized = {}
|
|
39
|
+
for key, value in (raw or {}).items():
|
|
40
|
+
canonical_key = "agi" if key == "agi_readiness" else key
|
|
41
|
+
if isinstance(value, dict):
|
|
42
|
+
normalized[canonical_key] = {
|
|
43
|
+
"current": int(value.get("current", 0) or 0),
|
|
44
|
+
"target": int(value.get("target", 0) or 0),
|
|
45
|
+
}
|
|
46
|
+
else:
|
|
47
|
+
normalized[canonical_key] = {
|
|
48
|
+
"current": 0,
|
|
49
|
+
"target": int(value or 0),
|
|
50
|
+
}
|
|
51
|
+
return normalized
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def normalize_objective(obj: dict | None) -> dict:
|
|
55
|
+
"""Upgrade legacy objective files to the canonical schema."""
|
|
56
|
+
source = dict(obj or {})
|
|
57
|
+
|
|
58
|
+
if "evolution_mode" in source:
|
|
59
|
+
mode = str(source.get("evolution_mode") or "auto").strip().lower()
|
|
60
|
+
else:
|
|
61
|
+
legacy_mode = str(source.get("review_mode") or "").strip().lower()
|
|
62
|
+
if legacy_mode in {"manual", "review"}:
|
|
63
|
+
mode = "review"
|
|
64
|
+
elif legacy_mode in {"managed", "hybrid", "owner", "core"}:
|
|
65
|
+
mode = "managed"
|
|
66
|
+
else:
|
|
67
|
+
mode = "auto"
|
|
68
|
+
|
|
69
|
+
if mode not in {"auto", "review", "managed"}:
|
|
70
|
+
mode = "auto"
|
|
71
|
+
|
|
72
|
+
dimensions = source.get("dimensions")
|
|
73
|
+
if not isinstance(dimensions, dict) or not dimensions:
|
|
74
|
+
dimensions = _normalize_dimensions(source.get("dimension_targets"))
|
|
75
|
+
else:
|
|
76
|
+
dimensions = _normalize_dimensions(dimensions)
|
|
77
|
+
|
|
78
|
+
defaults = {
|
|
79
|
+
"episodic_memory": {"current": 0, "target": 90},
|
|
80
|
+
"autonomy": {"current": 0, "target": 80},
|
|
81
|
+
"proactivity": {"current": 0, "target": 70},
|
|
82
|
+
"self_improvement": {"current": 0, "target": 60},
|
|
83
|
+
"agi": {"current": 0, "target": 20},
|
|
84
|
+
}
|
|
85
|
+
merged_dimensions = dict(defaults)
|
|
86
|
+
merged_dimensions.update(dimensions)
|
|
87
|
+
|
|
88
|
+
normalized = dict(source)
|
|
89
|
+
normalized["evolution_mode"] = mode
|
|
90
|
+
normalized["dimensions"] = merged_dimensions
|
|
91
|
+
normalized["total_evolutions"] = int(source.get("total_evolutions", source.get("cycles_completed", 0)) or 0)
|
|
92
|
+
normalized["last_evolution"] = source.get("last_evolution", source.get("last_cycle"))
|
|
93
|
+
normalized["total_proposals_made"] = int(source.get("total_proposals_made", 0) or 0)
|
|
94
|
+
normalized["total_auto_applied"] = int(source.get("total_auto_applied", 0) or 0)
|
|
95
|
+
normalized["consecutive_failures"] = int(source.get("consecutive_failures", 0) or 0)
|
|
96
|
+
normalized["history"] = source.get("history", []) if isinstance(source.get("history"), list) else []
|
|
97
|
+
normalized["evolution_enabled"] = bool(source.get("evolution_enabled", True))
|
|
98
|
+
normalized.pop("review_mode", None)
|
|
99
|
+
normalized.pop("dimension_targets", None)
|
|
100
|
+
normalized.pop("cycles_completed", None)
|
|
101
|
+
normalized.pop("last_cycle", None)
|
|
102
|
+
return normalized
|
|
103
|
+
|
|
104
|
+
|
|
37
105
|
def load_objective() -> dict:
|
|
38
106
|
if OBJECTIVE_FILE.exists():
|
|
39
|
-
return json.loads(OBJECTIVE_FILE.read_text())
|
|
40
|
-
return {}
|
|
107
|
+
return normalize_objective(json.loads(OBJECTIVE_FILE.read_text()))
|
|
108
|
+
return normalize_objective({})
|
|
41
109
|
|
|
42
110
|
|
|
43
111
|
def save_objective(obj: dict):
|
|
44
|
-
OBJECTIVE_FILE.
|
|
112
|
+
OBJECTIVE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
OBJECTIVE_FILE.write_text(json.dumps(normalize_objective(obj), indent=2, ensure_ascii=False))
|
|
45
114
|
|
|
46
115
|
|
|
47
116
|
def get_week_data(db_path: str) -> dict:
|
|
@@ -196,9 +265,18 @@ def build_evolution_prompt(week_data: dict, objective: dict) -> str:
|
|
|
196
265
|
"current_scores": {dim: m["score"] for dim, m in week_data.get("current_metrics", {}).items()},
|
|
197
266
|
}
|
|
198
267
|
|
|
199
|
-
mode = objective.get("evolution_mode", "auto")
|
|
268
|
+
mode = normalize_objective(objective).get("evolution_mode", "auto")
|
|
200
269
|
total = objective.get("total_evolutions", 0)
|
|
201
270
|
max_auto = max_auto_changes(total)
|
|
271
|
+
if mode == "review":
|
|
272
|
+
mode_desc = "review-only, nothing executes automatically"
|
|
273
|
+
safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/"
|
|
274
|
+
elif mode == "managed":
|
|
275
|
+
mode_desc = f"owner-managed, max {max_auto} auto-applied changes with rollback and followups"
|
|
276
|
+
safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/, NEXO_CODE/src, repo bin/docs/templates/tests"
|
|
277
|
+
else:
|
|
278
|
+
mode_desc = f"public auto, max {max_auto} auto-applied changes in personal safe zones"
|
|
279
|
+
safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/"
|
|
202
280
|
|
|
203
281
|
prompt = f"""You are NEXO Evolution — the weekly self-improvement cycle.
|
|
204
282
|
|
|
@@ -212,7 +290,7 @@ WEEK SUMMARY:
|
|
|
212
290
|
- {stats['evolution_history']} past evolution proposals
|
|
213
291
|
- Current scores: {json.dumps(stats['current_scores'])}
|
|
214
292
|
|
|
215
|
-
MODE: {mode} ({
|
|
293
|
+
MODE: {mode} ({mode_desc})
|
|
216
294
|
CYCLE: #{total + 1}
|
|
217
295
|
|
|
218
296
|
INVESTIGATE using these tools:
|
|
@@ -232,9 +310,11 @@ LOOK FOR:
|
|
|
232
310
|
- Patterns in self-critique that suggest systemic issues
|
|
233
311
|
|
|
234
312
|
SAFETY:
|
|
235
|
-
- Safe zones for
|
|
313
|
+
- Safe zones for this mode: {safe_zones}
|
|
236
314
|
- IMMUTABLE files (never touch): db.py, server.py, plugin_loader.py, cognitive.py, CLAUDE.md
|
|
237
315
|
- Every change needs: what file, what to change, why, risk, how to verify
|
|
316
|
+
- AUTO changes must be deterministic. If the edit is ambiguous, risky, or needs human taste, mark it as "propose".
|
|
317
|
+
- In managed mode, failed AUTO changes will be rolled back automatically and turned into followups with evidence.
|
|
238
318
|
|
|
239
319
|
OUTPUT FORMAT (JSON):
|
|
240
320
|
{{
|
package/src/plugins/evolution.py
CHANGED
|
@@ -43,8 +43,15 @@ def handle_evolution_history(limit: int = 10) -> str:
|
|
|
43
43
|
|
|
44
44
|
lines = [f"EVOLUTION HISTORY ({len(history)} entries):"]
|
|
45
45
|
for h in history:
|
|
46
|
-
status_icon = {
|
|
47
|
-
|
|
46
|
+
status_icon = {
|
|
47
|
+
"applied": "✓",
|
|
48
|
+
"rolled_back": "↺",
|
|
49
|
+
"blocked": "⛔",
|
|
50
|
+
"proposed": "?",
|
|
51
|
+
"pending_review": "…",
|
|
52
|
+
"accepted": "✓✓",
|
|
53
|
+
"rejected": "✗✗",
|
|
54
|
+
}.get(h["status"], "·")
|
|
48
55
|
lines.append(f" {status_icon} #{h['id']} [{h['classification']}] {h['dimension']}")
|
|
49
56
|
lines.append(f" {h['proposal'][:100]}")
|
|
50
57
|
if h.get("test_result"):
|
|
@@ -34,23 +34,6 @@ SANDBOX_DIR = CLAUDE_DIR / "sandbox" / "workspace"
|
|
|
34
34
|
MAX_CONSECUTIVE_FAILURES = 3
|
|
35
35
|
MAX_SNAPSHOTS = 8
|
|
36
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: user scripts and plugins only — NEVER core code
|
|
49
|
-
AUTO_SAFE_PREFIXES_PUBLIC = [
|
|
50
|
-
str(CLAUDE_DIR / "scripts") + "/",
|
|
51
|
-
str(CLAUDE_DIR / "plugins") + "/",
|
|
52
|
-
]
|
|
53
|
-
|
|
54
37
|
# ── Immutable files — NEVER touch (applies to ALL modes) ────────────────
|
|
55
38
|
IMMUTABLE_FILES = {
|
|
56
39
|
"db.py", "server.py", "plugin_loader.py", "nexo-watchdog.sh",
|
|
@@ -64,6 +47,52 @@ IMMUTABLE_FILES = {
|
|
|
64
47
|
"tools_task_history.py", "tools_menu.py",
|
|
65
48
|
}
|
|
66
49
|
|
|
50
|
+
|
|
51
|
+
def _repo_root() -> Path | None:
|
|
52
|
+
candidate = NEXO_CODE.parent
|
|
53
|
+
if (candidate / "package.json").exists():
|
|
54
|
+
return candidate
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _public_safe_prefixes() -> list[str]:
|
|
59
|
+
return [
|
|
60
|
+
str(CLAUDE_DIR / "scripts") + "/",
|
|
61
|
+
str(CLAUDE_DIR / "plugins") + "/",
|
|
62
|
+
str(CLAUDE_DIR / "skills") + "/",
|
|
63
|
+
str(CLAUDE_DIR / "skills-runtime") + "/",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _managed_safe_prefixes() -> list[str]:
|
|
68
|
+
prefixes = [
|
|
69
|
+
str(CLAUDE_DIR / "scripts") + "/",
|
|
70
|
+
str(CLAUDE_DIR / "plugins") + "/",
|
|
71
|
+
str(CLAUDE_DIR / "brain") + "/",
|
|
72
|
+
str(CLAUDE_DIR / "coordination") + "/",
|
|
73
|
+
str(CLAUDE_DIR / "logs") + "/",
|
|
74
|
+
str(CLAUDE_DIR / "skills") + "/",
|
|
75
|
+
str(CLAUDE_DIR / "skills-core") + "/",
|
|
76
|
+
str(CLAUDE_DIR / "skills-runtime") + "/",
|
|
77
|
+
str(NEXO_CODE) + "/",
|
|
78
|
+
]
|
|
79
|
+
repo_root = _repo_root()
|
|
80
|
+
if repo_root:
|
|
81
|
+
for rel in ("bin", "docs", "templates", "tests"):
|
|
82
|
+
prefixes.append(str(repo_root / rel) + "/")
|
|
83
|
+
return prefixes
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _normalize_mode(mode: str) -> str:
|
|
87
|
+
value = str(mode or "auto").strip().lower()
|
|
88
|
+
aliases = {
|
|
89
|
+
"owner": "managed",
|
|
90
|
+
"core": "managed",
|
|
91
|
+
"hybrid": "managed",
|
|
92
|
+
"manual": "review",
|
|
93
|
+
}
|
|
94
|
+
return aliases.get(value, value if value in {"auto", "review", "managed"} else "auto")
|
|
95
|
+
|
|
67
96
|
# ── Claude CLI path ──────────────────────────────────────────────────────
|
|
68
97
|
def _resolve_claude_cli() -> Path:
|
|
69
98
|
"""Find claude CLI: saved path > PATH > common locations."""
|
|
@@ -162,16 +191,18 @@ def call_claude_cli(prompt: str) -> str:
|
|
|
162
191
|
# ── File safety validation ───────────────────────────────────────────────
|
|
163
192
|
def is_safe_path(filepath: str, mode: str = "auto") -> bool:
|
|
164
193
|
"""Check if a file path is within safe zones and not immutable.
|
|
165
|
-
mode='auto' (public): restricted to
|
|
166
|
-
mode='
|
|
194
|
+
mode='auto' (public): restricted to personal automation surfaces.
|
|
195
|
+
mode='managed' (owner): broader repo/core surfaces with rollback.
|
|
196
|
+
mode='review': broader zones for proposal validation, but no execution.
|
|
167
197
|
"""
|
|
168
198
|
expanded = str(Path(filepath).expanduser().resolve())
|
|
169
199
|
filename = Path(expanded).name
|
|
200
|
+
mode = _normalize_mode(mode)
|
|
170
201
|
|
|
171
202
|
if filename in IMMUTABLE_FILES:
|
|
172
203
|
return False
|
|
173
204
|
|
|
174
|
-
prefixes =
|
|
205
|
+
prefixes = _managed_safe_prefixes() if mode in {"managed", "review"} else _public_safe_prefixes()
|
|
175
206
|
for prefix in prefixes:
|
|
176
207
|
resolved_prefix = str(Path(prefix).expanduser().resolve())
|
|
177
208
|
if expanded.startswith(resolved_prefix):
|
|
@@ -218,13 +249,13 @@ def validate_syntax(filepath: str) -> tuple[bool, str]:
|
|
|
218
249
|
|
|
219
250
|
|
|
220
251
|
# ── Apply a single change operation ──────────────────────────────────────
|
|
221
|
-
def apply_change(change: dict) -> tuple[bool, str]:
|
|
252
|
+
def apply_change(change: dict, mode: str = "auto") -> tuple[bool, str]:
|
|
222
253
|
"""Apply a single file change operation. Returns (success, message)."""
|
|
223
254
|
filepath = str(Path(change["file"]).expanduser())
|
|
224
255
|
operation = change.get("operation", "")
|
|
225
256
|
content = change.get("content", "")
|
|
226
257
|
|
|
227
|
-
if not is_safe_path(filepath):
|
|
258
|
+
if not is_safe_path(filepath, mode=mode):
|
|
228
259
|
return False, f"BLOCKED: {filepath} is outside safe zones or immutable"
|
|
229
260
|
|
|
230
261
|
try:
|
|
@@ -269,7 +300,7 @@ def apply_change(change: dict) -> tuple[bool, str]:
|
|
|
269
300
|
|
|
270
301
|
|
|
271
302
|
# ── Execute AUTO proposals ───────────────────────────────────────────────
|
|
272
|
-
def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connection) -> dict:
|
|
303
|
+
def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connection, mode: str = "auto") -> dict:
|
|
273
304
|
"""Execute an AUTO proposal with snapshot/apply/validate/rollback."""
|
|
274
305
|
changes = proposal.get("changes", [])
|
|
275
306
|
if not changes:
|
|
@@ -278,7 +309,7 @@ def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connecti
|
|
|
278
309
|
# Validate all paths first
|
|
279
310
|
for change in changes:
|
|
280
311
|
filepath = str(Path(change["file"]).expanduser())
|
|
281
|
-
if not is_safe_path(filepath):
|
|
312
|
+
if not is_safe_path(filepath, mode=mode):
|
|
282
313
|
return {"status": "blocked", "reason": f"Unsafe path: {filepath}"}
|
|
283
314
|
|
|
284
315
|
# Collect files to snapshot (existing files only)
|
|
@@ -299,7 +330,7 @@ def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connecti
|
|
|
299
330
|
all_results = []
|
|
300
331
|
try:
|
|
301
332
|
for change in changes:
|
|
302
|
-
success, msg = apply_change(change)
|
|
333
|
+
success, msg = apply_change(change, mode=mode)
|
|
303
334
|
all_results.append(msg)
|
|
304
335
|
log(f" {msg}")
|
|
305
336
|
if not success:
|
|
@@ -343,57 +374,102 @@ def execute_auto_proposal(proposal: dict, cycle_num: int, conn: sqlite3.Connecti
|
|
|
343
374
|
log(f" Removed created file: {filepath}")
|
|
344
375
|
|
|
345
376
|
return {
|
|
346
|
-
"status": "
|
|
377
|
+
"status": "rolled_back",
|
|
347
378
|
"snapshot_ref": snapshot_ref,
|
|
348
379
|
"files_changed": [],
|
|
349
380
|
"test_result": f"ROLLBACK: {e}; " + "; ".join(all_results),
|
|
350
381
|
}
|
|
351
382
|
|
|
352
383
|
|
|
353
|
-
# ──
|
|
354
|
-
def
|
|
355
|
-
|
|
356
|
-
|
|
384
|
+
# ── Followups for managed/review modes ──────────────────────────────────
|
|
385
|
+
def _insert_followup(conn: sqlite3.Connection, followup_id: str, description: str,
|
|
386
|
+
verification: str, due_date: str | None = None):
|
|
387
|
+
now_epoch = datetime.now().timestamp()
|
|
388
|
+
conn.execute(
|
|
389
|
+
"INSERT OR REPLACE INTO followups (id, description, date, status, verification, created_at, updated_at) "
|
|
390
|
+
"VALUES (?, ?, ?, 'PENDING', ?, ?, ?)",
|
|
391
|
+
(followup_id, description, due_date, verification, now_epoch, now_epoch)
|
|
392
|
+
)
|
|
393
|
+
conn.commit()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _create_cycle_followup(conn: sqlite3.Connection, cycle_num: int,
|
|
397
|
+
items: list[dict], analysis: str, mode: str):
|
|
398
|
+
"""Create a followup summarizing pending proposals or owner review items."""
|
|
357
399
|
tomorrow = (date.today() + timedelta(days=1)).isoformat()
|
|
358
400
|
followup_id = f"NF-EVO-C{cycle_num}"
|
|
359
401
|
|
|
360
402
|
public_items = [i for i in items if i.get("scope") == "public"]
|
|
361
403
|
local_items = [i for i in items if i.get("scope") != "public"]
|
|
362
404
|
|
|
363
|
-
|
|
405
|
+
title = "proposals to review" if mode == "review" else "items needing attention"
|
|
406
|
+
lines = [f"Evolution Cycle #{cycle_num} — {len(items)} {title}."]
|
|
364
407
|
lines.append(f"Analysis: {analysis[:200]}")
|
|
365
408
|
lines.append("")
|
|
366
409
|
|
|
367
410
|
if public_items:
|
|
368
411
|
lines.append(f"FOR EVERYONE ({len(public_items)}):")
|
|
369
412
|
for i, item in enumerate(public_items, 1):
|
|
370
|
-
|
|
413
|
+
status = item.get("status", "proposed").upper()
|
|
414
|
+
lines.append(f" {i}. [{status}] [{item['dimension']}] {item['action'][:120]}")
|
|
371
415
|
lines.append(f" Why: {item['reasoning'][:100]}")
|
|
416
|
+
if item.get("detail"):
|
|
417
|
+
lines.append(f" Detail: {item['detail'][:160]}")
|
|
372
418
|
lines.append("")
|
|
373
419
|
|
|
374
420
|
if local_items:
|
|
375
421
|
lines.append(f"FOR YOU ONLY ({len(local_items)}):")
|
|
376
422
|
for i, item in enumerate(local_items, 1):
|
|
377
|
-
|
|
423
|
+
status = item.get("status", "proposed").upper()
|
|
424
|
+
lines.append(f" {i}. [{status}] [{item['dimension']}] {item['action'][:120]}")
|
|
378
425
|
lines.append(f" Why: {item['reasoning'][:100]}")
|
|
426
|
+
if item.get("detail"):
|
|
427
|
+
lines.append(f" Detail: {item['detail'][:160]}")
|
|
379
428
|
|
|
380
429
|
description = "\n".join(lines)
|
|
381
430
|
|
|
382
431
|
try:
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
now_epoch, now_epoch)
|
|
432
|
+
_insert_followup(
|
|
433
|
+
conn,
|
|
434
|
+
followup_id,
|
|
435
|
+
description,
|
|
436
|
+
f"SELECT * FROM evolution_log WHERE cycle_number={cycle_num}",
|
|
437
|
+
due_date=tomorrow,
|
|
390
438
|
)
|
|
391
|
-
conn.commit()
|
|
392
439
|
log(f" Followup {followup_id} created for {tomorrow}")
|
|
393
440
|
except Exception as e:
|
|
394
441
|
log(f" WARN: Failed to create followup: {e}")
|
|
395
442
|
|
|
396
443
|
|
|
444
|
+
def _create_failure_followup(conn: sqlite3.Connection, cycle_num: int, log_id: int,
|
|
445
|
+
proposal: dict, result: dict):
|
|
446
|
+
"""Create an incident-style followup for a failed or blocked AUTO proposal."""
|
|
447
|
+
followup_id = f"NF-EVO-L{log_id}"
|
|
448
|
+
lines = [
|
|
449
|
+
f"Evolution AUTO proposal failed in cycle #{cycle_num}.",
|
|
450
|
+
f"Action: {proposal.get('action', '')[:200]}",
|
|
451
|
+
f"Dimension: {proposal.get('dimension', 'other')}",
|
|
452
|
+
f"Status: {result.get('status', 'failed')}",
|
|
453
|
+
f"Reason: {(result.get('reason') or result.get('test_result') or 'unknown')[:400]}",
|
|
454
|
+
]
|
|
455
|
+
snapshot_ref = result.get("snapshot_ref")
|
|
456
|
+
if snapshot_ref:
|
|
457
|
+
lines.append(f"Snapshot: {snapshot_ref}")
|
|
458
|
+
description = "\n".join(lines)
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
_insert_followup(
|
|
462
|
+
conn,
|
|
463
|
+
followup_id,
|
|
464
|
+
description,
|
|
465
|
+
f"SELECT * FROM evolution_log WHERE id={log_id}",
|
|
466
|
+
due_date=(date.today() + timedelta(days=1)).isoformat(),
|
|
467
|
+
)
|
|
468
|
+
log(f" Failure followup {followup_id} created")
|
|
469
|
+
except Exception as e:
|
|
470
|
+
log(f" WARN: Failed to create failure followup: {e}")
|
|
471
|
+
|
|
472
|
+
|
|
397
473
|
# ── Main run ─────────────────────────────────────────────────────────────
|
|
398
474
|
def run():
|
|
399
475
|
log("=" * 60)
|
|
@@ -482,14 +558,12 @@ def run():
|
|
|
482
558
|
max_auto = max_auto_changes(objective.get("total_evolutions", 0))
|
|
483
559
|
auto_count = 0
|
|
484
560
|
auto_applied = 0
|
|
485
|
-
evolution_mode = objective.get("evolution_mode", "auto")
|
|
561
|
+
evolution_mode = _normalize_mode(objective.get("evolution_mode", "auto"))
|
|
486
562
|
|
|
487
563
|
conn = sqlite3.connect(str(NEXO_DB), timeout=10)
|
|
488
564
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
489
565
|
|
|
490
|
-
|
|
491
|
-
# In "auto" mode: execute AUTO proposals, log PROPOSE as proposed
|
|
492
|
-
review_items = []
|
|
566
|
+
followup_items = []
|
|
493
567
|
|
|
494
568
|
for p in proposals:
|
|
495
569
|
classification = p.get("classification", "propose")
|
|
@@ -499,30 +573,29 @@ def run():
|
|
|
499
573
|
scope = p.get("scope", "local") # "public" or "local"
|
|
500
574
|
|
|
501
575
|
if evolution_mode == "review":
|
|
502
|
-
# Owner mode: nothing executes, everything queued for review
|
|
503
576
|
log(f" QUEUED [{scope}]: {action[:80]}")
|
|
504
577
|
conn.execute(
|
|
505
578
|
"INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
|
|
506
579
|
"reasoning, status) VALUES (?, ?, ?, ?, ?, ?)",
|
|
507
580
|
(cycle_num, dimension, action, classification, reasoning, "pending_review")
|
|
508
581
|
)
|
|
509
|
-
|
|
582
|
+
followup_items.append({
|
|
510
583
|
"dimension": dimension,
|
|
511
584
|
"action": action,
|
|
512
585
|
"reasoning": reasoning,
|
|
513
586
|
"scope": scope,
|
|
514
587
|
"classification": classification,
|
|
588
|
+
"status": "pending_review",
|
|
515
589
|
})
|
|
516
590
|
|
|
517
591
|
elif classification == "auto" and auto_count < max_auto:
|
|
518
|
-
# Public mode: execute AUTO proposals
|
|
519
592
|
auto_count += 1
|
|
520
593
|
log(f" AUTO #{auto_count}/{max_auto}: {action[:80]}")
|
|
521
594
|
|
|
522
|
-
result = execute_auto_proposal(p, cycle_num, conn)
|
|
595
|
+
result = execute_auto_proposal(p, cycle_num, conn, mode=evolution_mode)
|
|
523
596
|
status = result["status"]
|
|
524
597
|
|
|
525
|
-
conn.execute(
|
|
598
|
+
cur = conn.execute(
|
|
526
599
|
"INSERT INTO evolution_log (cycle_number, dimension, proposal, classification, "
|
|
527
600
|
"reasoning, status, files_changed, snapshot_ref, test_result) "
|
|
528
601
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
@@ -531,16 +604,20 @@ def run():
|
|
|
531
604
|
result.get("snapshot_ref", ""),
|
|
532
605
|
result.get("test_result", ""))
|
|
533
606
|
)
|
|
607
|
+
log_id = cur.lastrowid
|
|
534
608
|
|
|
535
609
|
if status == "applied":
|
|
536
610
|
auto_applied += 1
|
|
537
611
|
log(f" APPLIED successfully")
|
|
538
612
|
elif status == "blocked":
|
|
539
|
-
|
|
613
|
+
detail = result.get("reason") or result.get("test_result", "")
|
|
614
|
+
log(f" BLOCKED: {detail[:100]}")
|
|
615
|
+
_create_failure_followup(conn, cycle_num, log_id, p, result)
|
|
540
616
|
elif status == "skipped":
|
|
541
617
|
log(f" SKIPPED: {result.get('reason', '')}")
|
|
542
618
|
else:
|
|
543
|
-
log(f"
|
|
619
|
+
log(f" ROLLED BACK: {result.get('test_result', '')[:100]}")
|
|
620
|
+
_create_failure_followup(conn, cycle_num, log_id, p, result)
|
|
544
621
|
|
|
545
622
|
else:
|
|
546
623
|
# PROPOSE or over auto limit
|
|
@@ -555,12 +632,20 @@ def run():
|
|
|
555
632
|
"reasoning, status) VALUES (?, ?, ?, ?, ?, ?)",
|
|
556
633
|
(cycle_num, dimension, action, classification, reasoning, "proposed")
|
|
557
634
|
)
|
|
635
|
+
if evolution_mode in {"review", "managed"}:
|
|
636
|
+
followup_items.append({
|
|
637
|
+
"dimension": dimension,
|
|
638
|
+
"action": action,
|
|
639
|
+
"reasoning": reasoning,
|
|
640
|
+
"scope": scope,
|
|
641
|
+
"classification": classification,
|
|
642
|
+
"status": "proposed",
|
|
643
|
+
})
|
|
558
644
|
|
|
559
645
|
conn.commit()
|
|
560
646
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
_create_review_followup(conn, cycle_num, review_items, response.get("analysis", ""))
|
|
647
|
+
if evolution_mode in {"review", "managed"} and followup_items:
|
|
648
|
+
_create_cycle_followup(conn, cycle_num, followup_items, response.get("analysis", ""), evolution_mode)
|
|
564
649
|
|
|
565
650
|
# Update metrics
|
|
566
651
|
scores = response.get("dimension_scores", {})
|
|
@@ -591,6 +676,7 @@ def run():
|
|
|
591
676
|
objective.setdefault("history", []).insert(0, {
|
|
592
677
|
"cycle": cycle_num,
|
|
593
678
|
"date": str(date.today()),
|
|
679
|
+
"mode": evolution_mode,
|
|
594
680
|
"proposals": len(proposals),
|
|
595
681
|
"auto_count": auto_count,
|
|
596
682
|
"auto_applied": auto_applied,
|