nexo-brain 5.3.9 → 5.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/bin/nexo-brain.js +14 -0
- package/package.json +1 -1
- package/src/db/__init__.py +4 -1
- package/src/db/_protocol.py +28 -3
- package/src/doctor/providers/deep.py +61 -0
- package/src/evolution_cycle.py +30 -1
- package/src/plugins/cortex.py +37 -3
- package/src/plugins/evolution.py +56 -12
- package/src/plugins/protocol.py +41 -3
- package/src/scripts/nexo-synthesis.py +67 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.11",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `5.3.
|
|
21
|
+
Version `5.3.11` is the current packaged-runtime line: protocol and Cortex now reject malformed `outcome`, `task_type`, and `impact_level` values explicitly instead of silently coercing them into other valid states, so task history, debt, hot context, and decision telemetry stay trustworthy even when a caller passes a bad contract payload.
|
|
22
22
|
|
|
23
23
|
Start here:
|
|
24
24
|
- [5-minute quickstart](docs/quickstart-5-minutes.md)
|
|
@@ -89,7 +89,7 @@ Versions `3.1.7` through `3.2.0` close the recent-memory gap:
|
|
|
89
89
|
- when even that misses, NEXO now exposes raw transcript fallback tools for Claude Code and Codex session stores
|
|
90
90
|
- NEXO can now inspect itself through a live system catalog derived from canonical sources instead of relying only on stale docs or operator memory
|
|
91
91
|
|
|
92
|
-
Version `5.3.9` is the packaged core-artifact manifest heal for `5.3.8`: packaged updates now rebuild `runtime-core-artifacts.json` from the canonical npm package `src/` tree instead of scanning the live `~/.nexo/scripts` directory, script classification prefers that canonical packaged source when available, and runtime doctor syncs personal scripts before LaunchAgent inventory so personal automations recover cleanly instead of being mistaken for unknown core drift. Version `5.3.8` was the immediate packaged-migration hotfix for `5.3.7`: the installer/runtime migrator now discovers all top-level runtime Python modules from `src/` dynamically instead of relying on a manual allowlist, so new product surfaces like `nexo export` / `nexo import` actually arrive in `~/.nexo` after update instead of being present only in the published npm tarball. Version `5.3.7` closed the remaining packaged-runtime happy-path gap and finally exposed portable user-data migration commands: packaged `nexo update` now self-heals cron definitions and LaunchAgents after a successful npm bump, new `nexo export` / `nexo import` commands move operator data as a safe bundle instead of leaving that flow implicit, and runtime doctor now distinguishes tracked historical Codex drift from an actually broken runtime so cleaned installs stop staying red for stale transcript debt alone. Version `5.3.6` hardened the Claude Code bootstrap path and related runtime hygiene: managed client sync now writes the NEXO MCP server where current Claude Code actually reads it (`~/.claude.json`), script classification is stricter about core-vs-personal runtime artifacts, schedule status distinguishes genuinely running jobs from broken ones, and retroactive learnings stop opening keyword-only false positives outside their declared `applies_to` scope. Version `5.3.5` already keeps CLI version visibility honest right after `nexo update`: if the cached npm version lags behind the runtime you just installed, `nexo` / `nexo chat` now clamp `Latest` to the installed version and refresh the cache instead of showing a stale older release. Version `5.3.4` already cleaned up legacy core alias leakage and added the version-status banner. Version `5.3.3` closed the remaining packaged-runtime doctor mismatch: the built-in hourly backup helper is now inventoried as a core LaunchAgent, so clean installs no longer get a false unknown-LaunchAgent warning. Version `5.3.2` already hardened the runtime boundary by persisting which runtime scripts/hooks are core product artifacts, keeping `nexo scripts` from mixing those into the personal bucket, and migrating the legacy Claude Code heartbeat wrappers into managed core hooks.
|
|
92
|
+
Version `5.3.11` hardens protocol and Cortex contracts: malformed `outcome`, `task_type`, and `impact_level` values now fail explicitly instead of being coerced into other valid states, so persisted task history, debt, hot context, and decision telemetry stay faithful to what the caller actually asked for. Version `5.3.10` tightened the packaged-runtime truth layer again: installs and updates now keep `~/.nexo/package.json` aligned with the published npm package so runtime metadata and doctor evidence no longer drift to an old version, `nexo doctor --tier deep` treats a missing `self-audit-summary.json` as a pending bootstrap artifact when the runtime was just installed or updated instead of reporting a false degradation, weekly Evolution now asks for explicit `dimension_scores` / `score_evidence` so telemetry can persist instead of staying blank, and daily synthesis only ingests `update-last-summary.json` when it carries actionable runtime signals. Version `5.3.9` is the packaged core-artifact manifest heal for `5.3.8`: packaged updates now rebuild `runtime-core-artifacts.json` from the canonical npm package `src/` tree instead of scanning the live `~/.nexo/scripts` directory, script classification prefers that canonical packaged source when available, and runtime doctor syncs personal scripts before LaunchAgent inventory so personal automations recover cleanly instead of being mistaken for unknown core drift. Version `5.3.8` was the immediate packaged-migration hotfix for `5.3.7`: the installer/runtime migrator now discovers all top-level runtime Python modules from `src/` dynamically instead of relying on a manual allowlist, so new product surfaces like `nexo export` / `nexo import` actually arrive in `~/.nexo` after update instead of being present only in the published npm tarball. Version `5.3.7` closed the remaining packaged-runtime happy-path gap and finally exposed portable user-data migration commands: packaged `nexo update` now self-heals cron definitions and LaunchAgents after a successful npm bump, new `nexo export` / `nexo import` commands move operator data as a safe bundle instead of leaving that flow implicit, and runtime doctor now distinguishes tracked historical Codex drift from an actually broken runtime so cleaned installs stop staying red for stale transcript debt alone. Version `5.3.6` hardened the Claude Code bootstrap path and related runtime hygiene: managed client sync now writes the NEXO MCP server where current Claude Code actually reads it (`~/.claude.json`), script classification is stricter about core-vs-personal runtime artifacts, schedule status distinguishes genuinely running jobs from broken ones, and retroactive learnings stop opening keyword-only false positives outside their declared `applies_to` scope. Version `5.3.5` already keeps CLI version visibility honest right after `nexo update`: if the cached npm version lags behind the runtime you just installed, `nexo` / `nexo chat` now clamp `Latest` to the installed version and refresh the cache instead of showing a stale older release. Version `5.3.4` already cleaned up legacy core alias leakage and added the version-status banner. Version `5.3.3` closed the remaining packaged-runtime doctor mismatch: the built-in hourly backup helper is now inventoried as a core LaunchAgent, so clean installs no longer get a false unknown-LaunchAgent warning. Version `5.3.2` already hardened the runtime boundary by persisting which runtime scripts/hooks are core product artifacts, keeping `nexo scripts` from mixing those into the personal bucket, and migrating the legacy Claude Code heartbeat wrappers into managed core hooks.
|
|
93
93
|
|
|
94
94
|
Version `5.3.1` normalizes packaged npm installs so they behave like packaged npm installs: `nexo update` now keeps the runtime anchored to `~/.nexo`, refreshes packaged bootstrap/client artifacts after upgrade, avoids repo-only release-artifact drift in installed runtimes, and keeps personal scripts on the canonical packaged path.
|
|
95
95
|
|
package/bin/nexo-brain.js
CHANGED
|
@@ -120,6 +120,17 @@ function writeRuntimeCoreArtifactsManifest(nexoHome, srcDir) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
function syncRuntimePackageMetadata(repoRoot = path.join(__dirname, ".."), runtimeHome = NEXO_HOME) {
|
|
124
|
+
try {
|
|
125
|
+
const pkgSrc = path.join(repoRoot, "package.json");
|
|
126
|
+
if (fs.existsSync(pkgSrc)) {
|
|
127
|
+
fs.copyFileSync(pkgSrc, path.join(runtimeHome, "package.json"));
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
log(`WARN: could not sync runtime package metadata: ${err.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
123
134
|
function getCoreRuntimeFlatFiles(srcDir = path.join(__dirname, "..", "src")) {
|
|
124
135
|
const staticFiles = [
|
|
125
136
|
"server.py",
|
|
@@ -1606,6 +1617,7 @@ async function main() {
|
|
|
1606
1617
|
updated_at: new Date().toISOString(),
|
|
1607
1618
|
migrated_from: installedVersion,
|
|
1608
1619
|
}, null, 2));
|
|
1620
|
+
syncRuntimePackageMetadata(path.join(__dirname, ".."), NEXO_HOME);
|
|
1609
1621
|
|
|
1610
1622
|
// Save updated CLAUDE.md template as reference (don't overwrite user's)
|
|
1611
1623
|
const templateSrc = path.join(__dirname, "..", "templates", "CLAUDE.md.template");
|
|
@@ -1699,6 +1711,7 @@ async function main() {
|
|
|
1699
1711
|
fs.copyFileSync(srcFile, destFile);
|
|
1700
1712
|
}
|
|
1701
1713
|
});
|
|
1714
|
+
syncRuntimePackageMetadata(path.join(__dirname, ".."), NEXO_HOME);
|
|
1702
1715
|
|
|
1703
1716
|
const templatesSrc = path.join(__dirname, "..", "templates");
|
|
1704
1717
|
const templatesDest = path.join(NEXO_HOME, "templates");
|
|
@@ -2205,6 +2218,7 @@ async function main() {
|
|
|
2205
2218
|
files_updated: 0,
|
|
2206
2219
|
}, null, 2)
|
|
2207
2220
|
);
|
|
2221
|
+
syncRuntimePackageMetadata(path.join(__dirname, ".."), NEXO_HOME);
|
|
2208
2222
|
|
|
2209
2223
|
// Copy source files
|
|
2210
2224
|
log("Copying core runtime files...");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.11",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
package/src/db/__init__.py
CHANGED
|
@@ -147,13 +147,16 @@ from db._cron_runs import (
|
|
|
147
147
|
|
|
148
148
|
# Protocol discipline runtime
|
|
149
149
|
from db._protocol import (
|
|
150
|
+
VALID_IMPACT_LEVELS,
|
|
151
|
+
VALID_TASK_TYPES,
|
|
152
|
+
VALID_CLOSE_OUTCOMES,
|
|
150
153
|
create_protocol_task, get_protocol_task, close_protocol_task,
|
|
151
154
|
create_protocol_debt, resolve_protocol_debts, list_protocol_debts,
|
|
152
155
|
protocol_compliance_summary,
|
|
153
156
|
create_cortex_evaluation, get_cortex_evaluation, list_cortex_evaluations,
|
|
154
157
|
cortex_evaluation_summary,
|
|
155
158
|
latest_cortex_evaluation_for_task, task_has_cortex_evaluation,
|
|
156
|
-
override_cortex_evaluation,
|
|
159
|
+
override_cortex_evaluation, validate_close_outcome, validate_impact_level, validate_task_type,
|
|
157
160
|
)
|
|
158
161
|
|
|
159
162
|
# Durable workflow runtime
|
package/src/db/_protocol.py
CHANGED
|
@@ -9,6 +9,7 @@ from db._core import get_db
|
|
|
9
9
|
|
|
10
10
|
VALID_TASK_TYPES = {"answer", "analyze", "edit", "execute", "delegate"}
|
|
11
11
|
VALID_OUTCOMES = {"open", "done", "partial", "blocked", "failed", "cancelled"}
|
|
12
|
+
VALID_CLOSE_OUTCOMES = VALID_OUTCOMES - {"open"}
|
|
12
13
|
VALID_DEBT_STATUS = {"open", "forgiven", "resolved"}
|
|
13
14
|
VALID_IMPACT_LEVELS = {"medium", "high", "critical"}
|
|
14
15
|
|
|
@@ -33,6 +34,30 @@ def _row_to_dict(row):
|
|
|
33
34
|
return dict(row) if row else None
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
def validate_task_type(task_type: str) -> str:
|
|
38
|
+
clean_type = (task_type or "").strip()
|
|
39
|
+
if clean_type not in VALID_TASK_TYPES:
|
|
40
|
+
expected = ", ".join(sorted(VALID_TASK_TYPES))
|
|
41
|
+
raise ValueError(f"Invalid task_type '{clean_type or '<empty>'}'. Expected one of: {expected}.")
|
|
42
|
+
return clean_type
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def validate_impact_level(impact_level: str) -> str:
|
|
46
|
+
clean_level = (impact_level or "").strip()
|
|
47
|
+
if clean_level not in VALID_IMPACT_LEVELS:
|
|
48
|
+
expected = ", ".join(sorted(VALID_IMPACT_LEVELS))
|
|
49
|
+
raise ValueError(f"Invalid impact_level '{clean_level or '<empty>'}'. Expected one of: {expected}.")
|
|
50
|
+
return clean_level
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_close_outcome(outcome: str) -> str:
|
|
54
|
+
clean_outcome = (outcome or "").strip()
|
|
55
|
+
if clean_outcome not in VALID_CLOSE_OUTCOMES:
|
|
56
|
+
expected = ", ".join(sorted(VALID_CLOSE_OUTCOMES))
|
|
57
|
+
raise ValueError(f"Invalid close outcome '{clean_outcome or '<empty>'}'. Expected one of: {expected}.")
|
|
58
|
+
return clean_outcome
|
|
59
|
+
|
|
60
|
+
|
|
36
61
|
def create_protocol_task(
|
|
37
62
|
session_id: str,
|
|
38
63
|
goal: str,
|
|
@@ -68,7 +93,7 @@ def create_protocol_task(
|
|
|
68
93
|
) -> dict:
|
|
69
94
|
conn = get_db()
|
|
70
95
|
task_id = _task_id()
|
|
71
|
-
clean_type = task_type
|
|
96
|
+
clean_type = validate_task_type(task_type)
|
|
72
97
|
conn.execute(
|
|
73
98
|
"""INSERT INTO protocol_tasks (
|
|
74
99
|
task_id, session_id, goal, task_type, area, project_hint, context_hint,
|
|
@@ -144,7 +169,7 @@ def create_cortex_evaluation(
|
|
|
144
169
|
selection_source: str = "recommended",
|
|
145
170
|
) -> dict:
|
|
146
171
|
conn = get_db()
|
|
147
|
-
clean_level = impact_level
|
|
172
|
+
clean_level = validate_impact_level(impact_level)
|
|
148
173
|
cursor = conn.execute(
|
|
149
174
|
"""INSERT INTO cortex_evaluations (
|
|
150
175
|
session_id, task_id, goal, task_type, area, impact_level, context_hint,
|
|
@@ -335,7 +360,7 @@ def close_protocol_task(
|
|
|
335
360
|
outcome_notes: str = "",
|
|
336
361
|
) -> dict:
|
|
337
362
|
conn = get_db()
|
|
338
|
-
clean_outcome = outcome
|
|
363
|
+
clean_outcome = validate_close_outcome(outcome)
|
|
339
364
|
conn.execute(
|
|
340
365
|
"""UPDATE protocol_tasks
|
|
341
366
|
SET status = ?,
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
"""Deep tier checks — read existing artifacts for richer validation. Target <60s."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import datetime as dt
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import time
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
10
|
+
from cron_recovery import load_enabled_crons
|
|
9
11
|
from doctor.models import DoctorCheck, safe_check
|
|
10
12
|
|
|
11
13
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
12
14
|
|
|
13
15
|
# Freshness thresholds
|
|
14
16
|
SELF_AUDIT_FRESHNESS = 86400 * 2 # 2 days (runs daily)
|
|
17
|
+
SELF_AUDIT_BOOTSTRAP_GRACE = 86400 # 1 day grace after install/update before the first summary exists
|
|
15
18
|
PREFLIGHT_FRESHNESS = 86400 # 1 day
|
|
16
19
|
WATCHDOG_SMOKE_FRESHNESS = 86400 # 1 day
|
|
17
20
|
|
|
@@ -29,12 +32,70 @@ def _load_json(path: Path) -> dict:
|
|
|
29
32
|
return json.loads(path.read_text())
|
|
30
33
|
|
|
31
34
|
|
|
35
|
+
def _timestamp_age_seconds(value: str) -> float | None:
|
|
36
|
+
raw = str(value or "").strip()
|
|
37
|
+
if not raw:
|
|
38
|
+
return None
|
|
39
|
+
try:
|
|
40
|
+
parsed = dt.datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
|
41
|
+
except Exception:
|
|
42
|
+
return None
|
|
43
|
+
if parsed.tzinfo is None:
|
|
44
|
+
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
45
|
+
return max(0.0, time.time() - parsed.timestamp())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _runtime_bootstrap_age_seconds() -> float | None:
|
|
49
|
+
version_file = NEXO_HOME / "version.json"
|
|
50
|
+
try:
|
|
51
|
+
payload = _load_json(version_file)
|
|
52
|
+
except Exception:
|
|
53
|
+
payload = {}
|
|
54
|
+
for key in ("updated_at", "installed_at"):
|
|
55
|
+
age = _timestamp_age_seconds(str(payload.get(key, "") or ""))
|
|
56
|
+
if age is not None:
|
|
57
|
+
return age
|
|
58
|
+
return _file_age_seconds(version_file)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _self_audit_enabled() -> bool | None:
|
|
62
|
+
try:
|
|
63
|
+
return any(str(cron.get("id") or "").strip() == "self-audit" for cron in load_enabled_crons())
|
|
64
|
+
except Exception:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
32
68
|
def check_self_audit_summary() -> DoctorCheck:
|
|
33
69
|
"""Check latest self-audit summary exists and is recent."""
|
|
34
70
|
summary_file = NEXO_HOME / "logs" / "self-audit-summary.json"
|
|
35
71
|
age = _file_age_seconds(summary_file)
|
|
36
72
|
|
|
37
73
|
if age is None:
|
|
74
|
+
enabled = _self_audit_enabled()
|
|
75
|
+
if enabled is False:
|
|
76
|
+
return DoctorCheck(
|
|
77
|
+
id="deep.self_audit",
|
|
78
|
+
tier="deep",
|
|
79
|
+
status="healthy",
|
|
80
|
+
severity="info",
|
|
81
|
+
summary="Self-audit automation disabled or not installed",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
bootstrap_age = _runtime_bootstrap_age_seconds()
|
|
85
|
+
if enabled and bootstrap_age is not None and bootstrap_age <= SELF_AUDIT_BOOTSTRAP_GRACE:
|
|
86
|
+
bootstrap_hours = bootstrap_age / 3600
|
|
87
|
+
return DoctorCheck(
|
|
88
|
+
id="deep.self_audit",
|
|
89
|
+
tier="deep",
|
|
90
|
+
status="healthy",
|
|
91
|
+
severity="info",
|
|
92
|
+
summary="Self-audit scheduled but no summary yet",
|
|
93
|
+
evidence=[
|
|
94
|
+
f"Runtime install/update {bootstrap_hours:.0f} hours ago",
|
|
95
|
+
f"Expected later at: {summary_file}",
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
|
|
38
99
|
return DoctorCheck(
|
|
39
100
|
id="deep.self_audit",
|
|
40
101
|
tier="deep",
|
package/src/evolution_cycle.py
CHANGED
|
@@ -261,6 +261,19 @@ def dry_run_restore_test() -> bool:
|
|
|
261
261
|
def build_evolution_prompt(week_data: dict, objective: dict) -> str:
|
|
262
262
|
"""Build a SHORT prompt — CLI investigates on its own using tools."""
|
|
263
263
|
|
|
264
|
+
objective_dims = normalize_objective(objective).get("dimensions", {})
|
|
265
|
+
current_scores = {
|
|
266
|
+
dim: int(m["score"])
|
|
267
|
+
for dim, m in week_data.get("current_metrics", {}).items()
|
|
268
|
+
if isinstance(m, dict) and isinstance(m.get("score"), (int, float))
|
|
269
|
+
}
|
|
270
|
+
if not current_scores:
|
|
271
|
+
current_scores = {
|
|
272
|
+
dim: int((payload or {}).get("current", 0) or 0)
|
|
273
|
+
for dim, payload in objective_dims.items()
|
|
274
|
+
if isinstance(payload, dict)
|
|
275
|
+
}
|
|
276
|
+
|
|
264
277
|
# Summary stats only — CLI will dig deeper with tools
|
|
265
278
|
stats = {
|
|
266
279
|
"learnings_this_week": len(week_data.get("learnings", [])),
|
|
@@ -268,7 +281,7 @@ def build_evolution_prompt(week_data: dict, objective: dict) -> str:
|
|
|
268
281
|
"changes_this_week": len(week_data.get("changes", [])),
|
|
269
282
|
"diaries_this_week": len(week_data.get("diaries", [])),
|
|
270
283
|
"evolution_history": len(week_data.get("evolution_history", [])),
|
|
271
|
-
"current_scores":
|
|
284
|
+
"current_scores": current_scores,
|
|
272
285
|
}
|
|
273
286
|
|
|
274
287
|
mode = normalize_objective(objective).get("evolution_mode", "auto")
|
|
@@ -332,6 +345,20 @@ SAFETY:
|
|
|
332
345
|
OUTPUT FORMAT (JSON):
|
|
333
346
|
{{
|
|
334
347
|
"analysis": "one paragraph summary of what you found",
|
|
348
|
+
"dimension_scores": {{
|
|
349
|
+
"episodic_memory": 0,
|
|
350
|
+
"autonomy": 0,
|
|
351
|
+
"proactivity": 0,
|
|
352
|
+
"self_improvement": 0,
|
|
353
|
+
"agi": 0
|
|
354
|
+
}},
|
|
355
|
+
"score_evidence": {{
|
|
356
|
+
"episodic_memory": "why this score changed or stayed flat",
|
|
357
|
+
"autonomy": "why this score changed or stayed flat",
|
|
358
|
+
"proactivity": "why this score changed or stayed flat",
|
|
359
|
+
"self_improvement": "why this score changed or stayed flat",
|
|
360
|
+
"agi": "why this score changed or stayed flat"
|
|
361
|
+
}},
|
|
335
362
|
"patterns": [{{"type": "...", "description": "...", "frequency": "..."}}],
|
|
336
363
|
"proposals": [
|
|
337
364
|
{{
|
|
@@ -345,6 +372,8 @@ OUTPUT FORMAT (JSON):
|
|
|
345
372
|
]
|
|
346
373
|
}}
|
|
347
374
|
|
|
375
|
+
Always include all five canonical keys in `dimension_scores` and `score_evidence`.
|
|
376
|
+
Scores must be integers in the 0-100 range and reflect the current week, not targets.
|
|
348
377
|
Max 3 proposals. Quality over quantity. If nothing needs improving, say so."""
|
|
349
378
|
|
|
350
379
|
return prompt
|
package/src/plugins/cortex.py
CHANGED
|
@@ -22,6 +22,8 @@ import time
|
|
|
22
22
|
from datetime import datetime, timedelta
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
25
|
+
from db import VALID_IMPACT_LEVELS, VALID_TASK_TYPES, validate_impact_level, validate_task_type
|
|
26
|
+
|
|
25
27
|
|
|
26
28
|
def _get_db():
|
|
27
29
|
from db import get_db
|
|
@@ -734,9 +736,19 @@ def handle_cortex_check(
|
|
|
734
736
|
Returns:
|
|
735
737
|
Mode (ask/propose/act), available tools, warnings, and relevant Core Rules
|
|
736
738
|
"""
|
|
739
|
+
try:
|
|
740
|
+
clean_type = validate_task_type(task_type)
|
|
741
|
+
except ValueError as exc:
|
|
742
|
+
return "\n".join(
|
|
743
|
+
[
|
|
744
|
+
f"ERROR: {exc}",
|
|
745
|
+
f"Valid task types: {', '.join(sorted(VALID_TASK_TYPES))}",
|
|
746
|
+
]
|
|
747
|
+
)
|
|
748
|
+
|
|
737
749
|
state = {
|
|
738
750
|
"goal": goal.strip() if goal else "",
|
|
739
|
-
"task_type":
|
|
751
|
+
"task_type": clean_type,
|
|
740
752
|
"plan": _parse_json_list(plan),
|
|
741
753
|
"known_facts": _parse_json_list(known_facts),
|
|
742
754
|
"unknowns": _parse_json_list(unknowns),
|
|
@@ -860,8 +872,30 @@ def handle_cortex_decide(
|
|
|
860
872
|
indent=2,
|
|
861
873
|
)
|
|
862
874
|
|
|
863
|
-
|
|
864
|
-
|
|
875
|
+
try:
|
|
876
|
+
clean_type = validate_task_type(task_type)
|
|
877
|
+
except ValueError as exc:
|
|
878
|
+
return json.dumps(
|
|
879
|
+
{
|
|
880
|
+
"ok": False,
|
|
881
|
+
"error": str(exc),
|
|
882
|
+
"valid_task_types": sorted(VALID_TASK_TYPES),
|
|
883
|
+
},
|
|
884
|
+
ensure_ascii=False,
|
|
885
|
+
indent=2,
|
|
886
|
+
)
|
|
887
|
+
try:
|
|
888
|
+
clean_level = validate_impact_level(impact_level)
|
|
889
|
+
except ValueError as exc:
|
|
890
|
+
return json.dumps(
|
|
891
|
+
{
|
|
892
|
+
"ok": False,
|
|
893
|
+
"error": str(exc),
|
|
894
|
+
"valid_impact_levels": sorted(VALID_IMPACT_LEVELS),
|
|
895
|
+
},
|
|
896
|
+
ensure_ascii=False,
|
|
897
|
+
indent=2,
|
|
898
|
+
)
|
|
865
899
|
parsed_constraints = _parse_json_list(constraints)
|
|
866
900
|
parsed_evidence = _parse_json_list(evidence_refs)
|
|
867
901
|
try:
|
package/src/plugins/evolution.py
CHANGED
|
@@ -1,33 +1,77 @@
|
|
|
1
1
|
"""Evolution plugin — NEXO self-improvement tools for interactive sessions."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
from db import get_latest_metrics, get_evolution_history, update_evolution_log_status, get_db
|
|
5
7
|
|
|
6
8
|
|
|
9
|
+
CANONICAL_DIMENSIONS = {
|
|
10
|
+
"episodic_memory": "Episodic Memory",
|
|
11
|
+
"autonomy": "Autonomy",
|
|
12
|
+
"proactivity": "Proactivity",
|
|
13
|
+
"self_improvement": "Self-improvement",
|
|
14
|
+
"agi": "AGI",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _resolve_objective_file() -> Path:
|
|
19
|
+
nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
20
|
+
for candidate in (
|
|
21
|
+
nexo_home / "brain" / "evolution-objective.json",
|
|
22
|
+
nexo_home / "cortex" / "evolution-objective.json",
|
|
23
|
+
):
|
|
24
|
+
if candidate.exists():
|
|
25
|
+
return candidate
|
|
26
|
+
return nexo_home / "brain" / "evolution-objective.json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_objective() -> dict:
|
|
30
|
+
try:
|
|
31
|
+
raw = json.loads(_resolve_objective_file().read_text())
|
|
32
|
+
except Exception:
|
|
33
|
+
return {}
|
|
34
|
+
return raw if isinstance(raw, dict) else {}
|
|
35
|
+
|
|
36
|
+
|
|
7
37
|
def handle_evolution_status() -> str:
|
|
8
38
|
"""Show current NEXO dimension scores and recent trend."""
|
|
9
39
|
metrics = get_latest_metrics()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
BARS = {
|
|
14
|
-
"episodic_memory": "Episodic Memory",
|
|
15
|
-
"autonomy": "Autonomy",
|
|
16
|
-
"proactivity": "Proactivity",
|
|
17
|
-
"self_improvement": "Self-improvement",
|
|
18
|
-
"agi": "AGI",
|
|
19
|
-
}
|
|
40
|
+
objective = _load_objective()
|
|
41
|
+
objective_dims = objective.get("dimensions", {}) if isinstance(objective.get("dimensions"), dict) else {}
|
|
20
42
|
|
|
21
43
|
from user_context import get_context
|
|
22
44
|
lines = [f"{get_context().assistant_name} EVOLUTION STATUS:"]
|
|
23
|
-
|
|
45
|
+
has_output = False
|
|
46
|
+
for key, label in CANONICAL_DIMENSIONS.items():
|
|
24
47
|
m = metrics.get(key)
|
|
25
48
|
if m:
|
|
26
49
|
score = m["score"]
|
|
27
50
|
delta = m["delta"]
|
|
28
51
|
bar = "█" * (score // 5) + "░" * (20 - score // 5)
|
|
29
52
|
delta_str = f" (+{delta})" if delta > 0 else f" ({delta})" if delta < 0 else " (=)"
|
|
30
|
-
|
|
53
|
+
target = ""
|
|
54
|
+
if isinstance(objective_dims.get(key), dict) and objective_dims[key].get("target") is not None:
|
|
55
|
+
target = f" / target {int(objective_dims[key].get('target', 0) or 0)}%"
|
|
56
|
+
lines.append(f" {label:<20} {bar} {score}%{delta_str}{target}")
|
|
57
|
+
has_output = True
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
objective_entry = objective_dims.get(key)
|
|
61
|
+
if isinstance(objective_entry, dict):
|
|
62
|
+
score = int(objective_entry.get("current", 0) or 0)
|
|
63
|
+
target = int(objective_entry.get("target", 0) or 0)
|
|
64
|
+
bar = "█" * (score // 5) + "░" * (20 - score // 5)
|
|
65
|
+
lines.append(f" {label:<20} {bar} {score}% (objective fallback, target {target}%)")
|
|
66
|
+
has_output = True
|
|
67
|
+
|
|
68
|
+
if not has_output:
|
|
69
|
+
return "No evolution metrics recorded."
|
|
70
|
+
|
|
71
|
+
if not metrics:
|
|
72
|
+
lines.append(" Note: no persisted evolution_metrics rows yet; showing objective fallback.")
|
|
73
|
+
if objective.get("last_evolution"):
|
|
74
|
+
lines.append(f" Last evolution: {objective['last_evolution']}")
|
|
31
75
|
return "\n".join(lines)
|
|
32
76
|
|
|
33
77
|
|
package/src/plugins/protocol.py
CHANGED
|
@@ -10,6 +10,8 @@ import secrets
|
|
|
10
10
|
import time
|
|
11
11
|
|
|
12
12
|
from db import (
|
|
13
|
+
VALID_TASK_TYPES,
|
|
14
|
+
VALID_CLOSE_OUTCOMES,
|
|
13
15
|
close_protocol_task,
|
|
14
16
|
create_followup,
|
|
15
17
|
latest_cortex_evaluation_for_task,
|
|
@@ -28,6 +30,8 @@ from db import (
|
|
|
28
30
|
resolve_protocol_debts,
|
|
29
31
|
search_learnings,
|
|
30
32
|
task_has_cortex_evaluation,
|
|
33
|
+
validate_close_outcome,
|
|
34
|
+
validate_task_type,
|
|
31
35
|
)
|
|
32
36
|
from plugins.cortex import evaluate_cortex_state
|
|
33
37
|
from plugins.guard import handle_guard_check
|
|
@@ -651,7 +655,18 @@ def handle_confidence_check(
|
|
|
651
655
|
clean_goal = (goal or "").strip()
|
|
652
656
|
if not clean_goal:
|
|
653
657
|
return json.dumps({"ok": False, "error": "goal is required"}, ensure_ascii=False, indent=2)
|
|
654
|
-
|
|
658
|
+
try:
|
|
659
|
+
clean_type = validate_task_type(task_type)
|
|
660
|
+
except ValueError as exc:
|
|
661
|
+
return json.dumps(
|
|
662
|
+
{
|
|
663
|
+
"ok": False,
|
|
664
|
+
"error": str(exc),
|
|
665
|
+
"valid_task_types": sorted(VALID_TASK_TYPES),
|
|
666
|
+
},
|
|
667
|
+
ensure_ascii=False,
|
|
668
|
+
indent=2,
|
|
669
|
+
)
|
|
655
670
|
result = evaluate_response_confidence(
|
|
656
671
|
goal=clean_goal,
|
|
657
672
|
task_type=clean_type,
|
|
@@ -693,7 +708,18 @@ def handle_task_open(
|
|
|
693
708
|
if not clean_goal:
|
|
694
709
|
return json.dumps({"ok": False, "error": "goal is required"}, ensure_ascii=False, indent=2)
|
|
695
710
|
|
|
696
|
-
|
|
711
|
+
try:
|
|
712
|
+
clean_type = validate_task_type(task_type)
|
|
713
|
+
except ValueError as exc:
|
|
714
|
+
return json.dumps(
|
|
715
|
+
{
|
|
716
|
+
"ok": False,
|
|
717
|
+
"error": str(exc),
|
|
718
|
+
"valid_task_types": sorted(VALID_TASK_TYPES),
|
|
719
|
+
},
|
|
720
|
+
ensure_ascii=False,
|
|
721
|
+
indent=2,
|
|
722
|
+
)
|
|
697
723
|
files_list = _parse_list(files)
|
|
698
724
|
protocol_strictness = get_protocol_strictness()
|
|
699
725
|
if protocol_strictness in {"strict", "learning"} and clean_type == "edit" and not files_list:
|
|
@@ -949,7 +975,19 @@ def handle_task_close(
|
|
|
949
975
|
indent=2,
|
|
950
976
|
)
|
|
951
977
|
|
|
952
|
-
|
|
978
|
+
try:
|
|
979
|
+
clean_outcome = validate_close_outcome(outcome)
|
|
980
|
+
except ValueError as exc:
|
|
981
|
+
return json.dumps(
|
|
982
|
+
{
|
|
983
|
+
"ok": False,
|
|
984
|
+
"error": str(exc),
|
|
985
|
+
"task_id": task_id,
|
|
986
|
+
"valid_outcomes": sorted(VALID_CLOSE_OUTCOMES),
|
|
987
|
+
},
|
|
988
|
+
ensure_ascii=False,
|
|
989
|
+
indent=2,
|
|
990
|
+
)
|
|
953
991
|
clean_evidence = (evidence or "").strip()
|
|
954
992
|
files_changed_list = _parse_list(files_changed)
|
|
955
993
|
planned_files = _parse_list(task.get("files") or "[]")
|
|
@@ -135,6 +135,36 @@ def _impact_reasoning(row: dict) -> str:
|
|
|
135
135
|
return str(factors.get("reasoning") or "").strip()
|
|
136
136
|
|
|
137
137
|
|
|
138
|
+
def _load_json_summary(path: Path, *, actionable) -> tuple[dict | None, str | None]:
|
|
139
|
+
if not path.exists():
|
|
140
|
+
return None, None
|
|
141
|
+
try:
|
|
142
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
143
|
+
except Exception as exc:
|
|
144
|
+
return None, str(exc)
|
|
145
|
+
if not isinstance(payload, dict):
|
|
146
|
+
return None, "summary payload is not a JSON object"
|
|
147
|
+
if not actionable(payload):
|
|
148
|
+
return None, None
|
|
149
|
+
return payload, None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _load_coordination_summary(filename: str, *, actionable) -> tuple[dict | None, str | None]:
|
|
153
|
+
return _load_json_summary(COORD_DIR / filename, actionable=actionable)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _update_summary_actionable(payload: dict) -> bool:
|
|
157
|
+
if any(payload.get(key) for key in ("error", "updated", "deferred_reason", "git_update", "npm_notice")):
|
|
158
|
+
return True
|
|
159
|
+
for action in payload.get("actions") or []:
|
|
160
|
+
if str(action).startswith("personal-schedules-"):
|
|
161
|
+
return True
|
|
162
|
+
for message in payload.get("client_bootstrap_updates") or []:
|
|
163
|
+
if "already current" not in str(message).lower():
|
|
164
|
+
return True
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
138
168
|
def collect_data() -> dict:
|
|
139
169
|
"""Collect all raw data for synthesis."""
|
|
140
170
|
data = {"date": TODAY_STR}
|
|
@@ -207,6 +237,43 @@ def collect_data() -> dict:
|
|
|
207
237
|
except Exception as exc:
|
|
208
238
|
data["impact_queue_summary_error"] = str(exc)
|
|
209
239
|
|
|
240
|
+
followup_hygiene_summary, followup_hygiene_error = _load_coordination_summary(
|
|
241
|
+
"followup-hygiene-summary.json",
|
|
242
|
+
actionable=lambda payload: any(
|
|
243
|
+
int(payload.get(key, 0) or 0) > 0
|
|
244
|
+
for key in ("dirty_normalized", "stale_count", "orphan_count")
|
|
245
|
+
),
|
|
246
|
+
)
|
|
247
|
+
if followup_hygiene_summary is not None:
|
|
248
|
+
data["followup_hygiene_summary"] = followup_hygiene_summary
|
|
249
|
+
elif followup_hygiene_error:
|
|
250
|
+
data["followup_hygiene_summary_error"] = followup_hygiene_error
|
|
251
|
+
|
|
252
|
+
outcome_checker_summary, outcome_checker_error = _load_coordination_summary(
|
|
253
|
+
"outcome-checker-summary.json",
|
|
254
|
+
actionable=lambda payload: (
|
|
255
|
+
any(
|
|
256
|
+
int(payload.get(key, 0) or 0) > 0
|
|
257
|
+
for key in ("checked", "met", "missed", "pending", "errors")
|
|
258
|
+
)
|
|
259
|
+
or bool(payload.get("ids"))
|
|
260
|
+
or bool(((payload.get("auto_promoted_patterns") or {}).get("promoted") or []))
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
if outcome_checker_summary is not None:
|
|
264
|
+
data["outcome_checker_summary"] = outcome_checker_summary
|
|
265
|
+
elif outcome_checker_error:
|
|
266
|
+
data["outcome_checker_summary_error"] = outcome_checker_error
|
|
267
|
+
|
|
268
|
+
update_summary, update_summary_error = _load_json_summary(
|
|
269
|
+
NEXO_HOME / "logs" / "update-last-summary.json",
|
|
270
|
+
actionable=_update_summary_actionable,
|
|
271
|
+
)
|
|
272
|
+
if update_summary is not None:
|
|
273
|
+
data["update_summary"] = update_summary
|
|
274
|
+
elif update_summary_error:
|
|
275
|
+
data["update_summary_error"] = update_summary_error
|
|
276
|
+
|
|
210
277
|
# Guard stats
|
|
211
278
|
data["guard_stats"] = safe_query(
|
|
212
279
|
"SELECT category, COUNT(*) as cnt FROM learnings WHERE status='active' "
|