nexo-brain 7.1.4 → 7.1.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 +1 -1
- package/README.md +3 -3
- package/bin/postinstall.js +2 -2
- package/package.json +1 -1
- package/src/agent_runner.py +4 -32
- package/src/auto_update.py +120 -1
- package/src/automation_controls.py +2 -1
- package/src/autonomy_mandate.py +17 -2
- package/src/bootstrap_docs.py +2 -1
- package/src/calibration_runtime.py +46 -0
- package/src/claude_cli.py +151 -0
- package/src/cli.py +114 -0
- package/src/client_preferences.py +84 -1
- package/src/client_sync.py +48 -2
- package/src/cognitive/_search.py +109 -10
- package/src/cognitive/_trust.py +117 -0
- package/src/core_schedule_controls.py +494 -0
- package/src/cron_recovery.py +11 -2
- package/src/crons/sync.py +10 -1
- package/src/db/__init__.py +1 -0
- package/src/db/_learnings.py +21 -9
- package/src/db/_protocol.py +33 -2
- package/src/db/_reminders.py +23 -13
- package/src/db/_schema.py +4 -0
- package/src/db/_semantic_similarity.py +98 -0
- package/src/db/_skills.py +28 -18
- package/src/doctor/providers/runtime.py +54 -9
- package/src/email_config.py +5 -0
- package/src/enforcement_engine.py +151 -2
- package/src/guard_verbal_ack.py +66 -0
- package/src/hook_guardrails.py +240 -27
- package/src/hooks/auto_capture.py +32 -7
- package/src/hooks/post_tool_use.py +8 -5
- package/src/paths.py +9 -0
- package/src/plugin_loader.py +65 -27
- package/src/plugins/core_rules.py +30 -1
- package/src/plugins/cortex.py +4 -1
- package/src/plugins/doctor.py +15 -0
- package/src/plugins/episodic_memory.py +52 -2
- package/src/plugins/evolution.py +22 -0
- package/src/plugins/guard.py +282 -26
- package/src/plugins/personal_scripts.py +116 -15
- package/src/plugins/protocol.py +358 -55
- package/src/plugins/skills.py +11 -2
- package/src/product_mode.py +28 -6
- package/src/scripts/check-context.py +3 -2
- package/src/scripts/nexo-catchup.py +6 -25
- package/src/scripts/nexo-daily-self-audit.py +0 -21
- package/src/scripts/nexo-email-monitor.py +51 -10
- package/src/scripts/nexo-evolution-run.py +5 -22
- package/src/scripts/nexo-postmortem-consolidator.py +0 -21
- package/src/scripts/nexo-send-reply.py +40 -0
- package/src/scripts/nexo-sleep.py +0 -20
- package/src/scripts/nexo-synthesis.py +0 -21
- package/src/scripts/nexo-watchdog.sh +28 -30
- package/src/server.py +4 -86
- package/src/session_end_intent.py +31 -0
- package/src/skills/create-nexo-primitive/guide.md +87 -0
- package/src/skills/create-nexo-primitive/skill.json +62 -0
- package/src/tools_drive.py +277 -12
- package/src/tools_email_guard.py +74 -0
- package/src/tools_hot_context.py +11 -2
- package/src/tools_sessions.py +61 -9
- package/src/tools_system_catalog.py +2 -2
- package/src/user_context.py +2 -1
- package/templates/CLAUDE.md.template +1 -1
- package/templates/CODEX.AGENTS.md.template +1 -1
- package/templates/core-prompts/automation-backend-probe.md +1 -0
- package/templates/core-prompts/autonomy-mandate-question.md +6 -0
- package/templates/core-prompts/codex-protocol-contract.md +7 -0
- package/templates/core-prompts/drive-area-classifier-system.md +4 -0
- package/templates/core-prompts/drive-area-classifier-user.md +6 -0
- package/templates/core-prompts/email-monitor.md +2 -0
- package/templates/core-prompts/guard-verbal-ack-question.md +1 -0
- package/templates/core-prompts/heartbeat-diary-overdue.md +1 -0
- package/templates/core-prompts/heartbeat-guard-reminder.md +1 -0
- package/templates/core-prompts/heartbeat-learning-reminder.md +1 -0
- package/templates/core-prompts/hook-protocol-warning-guard-required.md +1 -0
- package/templates/core-prompts/hook-protocol-warning-heartbeat-close-evidence.md +1 -0
- package/templates/core-prompts/hook-protocol-warning-startup-required.md +1 -0
- package/templates/core-prompts/hook-protocol-warning-task-close-evidence.md +1 -0
- package/templates/core-prompts/hook-protocol-warning-task-open-guard-note.md +1 -0
- package/templates/core-prompts/hook-protocol-warning-task-open-required.md +1 -0
- package/templates/core-prompts/hook-protocol-warning-workflow-required.md +1 -0
- package/templates/core-prompts/post-tool-inbox-reminder.md +1 -0
- package/templates/core-prompts/server-mcp-instructions.md +38 -0
- package/templates/core-prompts/session-end-intent-question.md +1 -0
- package/templates/core-prompts/watchdog-repair.md +25 -0
- package/templates/launchagents/README.md +1 -1
- package/templates/launchagents/com.nexo.synthesis.plist +8 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.7",
|
|
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,16 +18,16 @@
|
|
|
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 `7.1.
|
|
21
|
+
Version `7.1.7` is the current packaged-runtime line. It hardens operator-facing email automation language: `email-monitor` now carries the calibrated operator language through its prompt contract and localizes direct `needs_interactive` escalation emails, so Spanish operators stop receiving fallback English monitor mail. No coordinated Desktop release was needed for this patch; the fix lives in Brain.
|
|
22
22
|
|
|
23
23
|
Previously in `7.0.1`: hotfix over v7.0.0 (db._core.DB_PATH was only caller still hardcoded to legacy ~/.nexo/data/nexo.db; every shared-DB command silently returned empty results post-migration). Previously in `7.0.0`: **BREAKING — Plan Consolidado fase F0.6**: physical separation of the runtime tree into `~/.nexo/{core,personal,runtime}/`. The flat layout (`~/.nexo/scripts/`, `brain/`, `data/`, `operations/`, ...) is gone. Operators on v6.x are auto-migrated on first `nexo update`; fresh installs land directly in the new tree. New `paths.py` helpers are transition-aware.
|
|
24
24
|
|
|
25
25
|
Previously in `6.5.0`: Plan Consolidado fase F0.2: operators can now `nexo scripts enable|disable|status <name>` any personal automation. The cron wrapper honours the flag at every tick (`exit 0` with `summary='[disabled]'` while the LaunchAgent stays loaded). The companion NEXO Desktop client (a closed-source product, distributed separately) wires the same toggle into its Automatizaciones panel. See [CHANGELOG](CHANGELOG.md) for the full diff.
|
|
26
26
|
|
|
27
|
-
> **About NEXO Desktop.** NEXO Desktop is a separate closed-source companion app distributed at [
|
|
27
|
+
> **About NEXO Desktop.** NEXO Desktop is a separate closed-source companion app distributed at [nexo-desktop.com](https://nexo-desktop.com/) — its source does not live in this repo. When release notes mention Desktop they describe a coordinated client release that consumes the Brain's CLI / MCP contract; the Brain itself is fully usable on its own (terminal, Codex, Claude Code, or any MCP client). If you want the product edition rather than the open-source Brain alone, contact `info@wazion.com` and ask about NEXO Desktop.
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
Previously in `6.4.0`: Plan Consolidado fase F1 — multi-tenant email accounts (`email_accounts` table, `nexo email setup` interactive wizard, `nexo email add --password-stdin --json` for machine consumers, idempotent migrator from legacy `~/.nexo/nexo-email/config.json`).
|
|
30
|
+
Previously in `6.4.0`: Plan Consolidado fase F1 — multi-tenant email accounts (`email_accounts` table, `nexo email setup` interactive wizard, `nexo email add --password-stdin --json` for machine consumers, idempotent migrator from legacy `~/.nexo/nexo-email/config.json`). On post-F0.6 installs that legacy-looking path is only a compatibility alias/shim into `~/.nexo/runtime/nexo-email/config.json`; it should never be treated as a second source of truth.
|
|
31
31
|
|
|
32
32
|
Previously in `6.3.1`: privacy hotfix over v6.3.0. The nightly auditor caught that `src/presets/entities_universal.json` in v6.3.0 shipped operator-specific `vhost_mapping` entries (private IPs, hostnames, tenant names). v6.3.1 pulls those out into `src/presets/entities_local.sample.json` (template) + `.gitignore`'d `~/.nexo/brain/presets/entities_local.json` (operator copy), and the installer drops the sample at `nexo init`. No behaviour change on the Guardian side.
|
|
33
33
|
|
package/bin/postinstall.js
CHANGED
|
@@ -32,9 +32,9 @@ if (fs.existsSync(VERSION_FILE)) {
|
|
|
32
32
|
// Run the main installer in --yes mode (non-interactive)
|
|
33
33
|
// It will detect the existing version and do migration only
|
|
34
34
|
// Let errors propagate so npm reports the failure correctly
|
|
35
|
-
const {
|
|
35
|
+
const { execFileSync } = require("child_process");
|
|
36
36
|
try {
|
|
37
|
-
|
|
37
|
+
execFileSync(process.execPath, [path.join(__dirname, "nexo-brain.js"), "--yes"], {
|
|
38
38
|
stdio: "inherit",
|
|
39
39
|
env: { ...process.env, NEXO_POSTINSTALL: "1", NEXO_HOME: NEXO_HOME }
|
|
40
40
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.7",
|
|
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/agent_runner.py
CHANGED
|
@@ -30,6 +30,7 @@ from client_preferences import (
|
|
|
30
30
|
resolve_client_runtime_profile,
|
|
31
31
|
resolve_terminal_client,
|
|
32
32
|
)
|
|
33
|
+
from claude_cli import resolve_claude_cli as _shared_resolve_claude_cli
|
|
33
34
|
from core_prompts import render_core_prompt
|
|
34
35
|
|
|
35
36
|
|
|
@@ -359,25 +360,7 @@ def _record_automation_run(
|
|
|
359
360
|
|
|
360
361
|
|
|
361
362
|
def _resolve_claude_cli() -> str:
|
|
362
|
-
|
|
363
|
-
if saved.exists():
|
|
364
|
-
candidate = saved.read_text().strip()
|
|
365
|
-
if candidate and Path(candidate).exists():
|
|
366
|
-
return candidate
|
|
367
|
-
env_path = os.environ.get("CLAUDE_BIN", "").strip()
|
|
368
|
-
if env_path and Path(env_path).exists():
|
|
369
|
-
return env_path
|
|
370
|
-
discovered = shutil.which("claude")
|
|
371
|
-
if discovered:
|
|
372
|
-
return discovered
|
|
373
|
-
for candidate in (
|
|
374
|
-
Path.home() / ".local" / "bin" / "claude",
|
|
375
|
-
Path.home() / ".npm-global" / "bin" / "claude",
|
|
376
|
-
Path("/usr/local/bin/claude"),
|
|
377
|
-
):
|
|
378
|
-
if candidate.exists():
|
|
379
|
-
return str(candidate)
|
|
380
|
-
return ""
|
|
363
|
+
return _shared_resolve_claude_cli()
|
|
381
364
|
|
|
382
365
|
|
|
383
366
|
def _resolve_codex_cli() -> str:
|
|
@@ -781,18 +764,7 @@ def _build_codex_prompt(
|
|
|
781
764
|
append_system_prompt: str = "",
|
|
782
765
|
allowed_tools: str = "",
|
|
783
766
|
) -> str:
|
|
784
|
-
protocol_contract = (
|
|
785
|
-
"NEXO PROTOCOL (MANDATORY):\n"
|
|
786
|
-
"- Before non-trivial analyze/edit/execute/delegate work, call `nexo_task_open(...)`. "
|
|
787
|
-
"If that tool is unavailable, call `nexo_guard_check(...)` and `nexo_cortex_check(...)` first.\n"
|
|
788
|
-
"- For long multi-step or cross-session work, call `nexo_workflow_open(...)` and keep it updated with "
|
|
789
|
-
"`nexo_workflow_update(...)` so resume/replay use durable state instead of guesswork.\n"
|
|
790
|
-
"- Before diagnosing NEXO, explicitly fix the plane first: `product_public`, `runtime_personal`, `installation_live`, `database_real`, or `cooperator`. "
|
|
791
|
-
"Do not mix planes inside the same diagnosis.\n"
|
|
792
|
-
"- If a target file has conditioned learnings or blocking guard rules, review them before any read/edit/delete step, and acknowledge guard before any edit/delete step.\n"
|
|
793
|
-
"- Do not claim done without explicit verification evidence. Close with `nexo_task_close(...)`; if unavailable, capture the change log and state the evidence explicitly.\n"
|
|
794
|
-
"- When a correction changes the canonical rule, capture or supersede the learning instead of leaving contradictory active rules behind."
|
|
795
|
-
)
|
|
767
|
+
protocol_contract = render_core_prompt("codex-protocol-contract")
|
|
796
768
|
instructions: list[str] = []
|
|
797
769
|
instructions.append(protocol_contract)
|
|
798
770
|
if append_system_prompt:
|
|
@@ -1217,7 +1189,7 @@ def probe_automation_backend(
|
|
|
1217
1189
|
}
|
|
1218
1190
|
try:
|
|
1219
1191
|
result = run_automation_prompt(
|
|
1220
|
-
"
|
|
1192
|
+
render_core_prompt("automation-backend-probe"),
|
|
1221
1193
|
backend=selected_backend,
|
|
1222
1194
|
cwd=cwd,
|
|
1223
1195
|
timeout=timeout,
|
package/src/auto_update.py
CHANGED
|
@@ -2229,6 +2229,7 @@ def _f06_legacy_shim_map() -> list[tuple[str, Path]]:
|
|
|
2229
2229
|
("data", NEXO_HOME / "runtime" / "data"),
|
|
2230
2230
|
("logs", NEXO_HOME / "runtime" / "logs"),
|
|
2231
2231
|
("operations", NEXO_HOME / "runtime" / "operations"),
|
|
2232
|
+
("state", NEXO_HOME / "runtime" / "state"),
|
|
2232
2233
|
("backups", NEXO_HOME / "runtime" / "backups"),
|
|
2233
2234
|
("memory", NEXO_HOME / "runtime" / "memory"),
|
|
2234
2235
|
("coordination", NEXO_HOME / "runtime" / "coordination"),
|
|
@@ -2236,6 +2237,14 @@ def _f06_legacy_shim_map() -> list[tuple[str, Path]]:
|
|
|
2236
2237
|
("nexo-email", NEXO_HOME / "runtime" / "nexo-email"),
|
|
2237
2238
|
("snapshots", NEXO_HOME / "runtime" / "snapshots"),
|
|
2238
2239
|
("crons", NEXO_HOME / "runtime" / "crons"),
|
|
2240
|
+
("workdir", paths.personal_lib_dir() / "workdir"),
|
|
2241
|
+
("working", paths.personal_lib_dir() / "working"),
|
|
2242
|
+
]
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
def _f06_legacy_file_shim_map() -> list[tuple[str, Path]]:
|
|
2246
|
+
return [
|
|
2247
|
+
("CLAUDE.md.generated", paths.personal_lib_dir() / "generated" / "CLAUDE.md.generated"),
|
|
2239
2248
|
]
|
|
2240
2249
|
|
|
2241
2250
|
|
|
@@ -2270,6 +2279,7 @@ def _f06_live_legacy_paths() -> list[Path]:
|
|
|
2270
2279
|
"brain",
|
|
2271
2280
|
"data",
|
|
2272
2281
|
"operations",
|
|
2282
|
+
"state",
|
|
2273
2283
|
"logs",
|
|
2274
2284
|
"backups",
|
|
2275
2285
|
"memory",
|
|
@@ -2287,6 +2297,8 @@ def _f06_live_legacy_paths() -> list[Path]:
|
|
|
2287
2297
|
"db",
|
|
2288
2298
|
"dashboard",
|
|
2289
2299
|
"skills-core",
|
|
2300
|
+
"workdir",
|
|
2301
|
+
"working",
|
|
2290
2302
|
)
|
|
2291
2303
|
]
|
|
2292
2304
|
present = [p for p in legacy_anchors if p.exists() and not p.is_symlink()]
|
|
@@ -2297,6 +2309,10 @@ def _f06_live_legacy_paths() -> list[Path]:
|
|
|
2297
2309
|
candidate = NEXO_HOME / legacy_name
|
|
2298
2310
|
if candidate.exists() and not candidate.is_symlink():
|
|
2299
2311
|
live_files.append(candidate)
|
|
2312
|
+
for legacy_name, _canonical in _f06_legacy_file_shim_map():
|
|
2313
|
+
candidate = NEXO_HOME / legacy_name
|
|
2314
|
+
if candidate.exists() and not candidate.is_symlink():
|
|
2315
|
+
live_files.append(candidate)
|
|
2300
2316
|
return live_files
|
|
2301
2317
|
|
|
2302
2318
|
|
|
@@ -2554,6 +2570,51 @@ def _ensure_f06_legacy_shims() -> None:
|
|
|
2554
2570
|
except Exception as exc:
|
|
2555
2571
|
_log(f"[F0.6 shim] file symlink create failed for {legacy_name}: {exc}")
|
|
2556
2572
|
|
|
2573
|
+
for legacy_name, canonical in _f06_legacy_file_shim_map():
|
|
2574
|
+
legacy = NEXO_HOME / legacy_name
|
|
2575
|
+
canonical.parent.mkdir(parents=True, exist_ok=True)
|
|
2576
|
+
|
|
2577
|
+
if legacy.is_symlink():
|
|
2578
|
+
try:
|
|
2579
|
+
if legacy.resolve(strict=False) == canonical.resolve(strict=False):
|
|
2580
|
+
continue
|
|
2581
|
+
except Exception:
|
|
2582
|
+
pass
|
|
2583
|
+
try:
|
|
2584
|
+
legacy.unlink()
|
|
2585
|
+
except Exception as exc:
|
|
2586
|
+
_log(f"[F0.6 shim] could not replace misc file symlink {legacy_name}: {exc}")
|
|
2587
|
+
continue
|
|
2588
|
+
|
|
2589
|
+
if legacy.is_file():
|
|
2590
|
+
if canonical.exists():
|
|
2591
|
+
if _same_file(legacy, canonical):
|
|
2592
|
+
try:
|
|
2593
|
+
legacy.unlink()
|
|
2594
|
+
except Exception:
|
|
2595
|
+
pass
|
|
2596
|
+
else:
|
|
2597
|
+
backup_target = _conflict_dir() / "legacy-files"
|
|
2598
|
+
backup_target.mkdir(parents=True, exist_ok=True)
|
|
2599
|
+
shutil.move(str(legacy), str(backup_target / legacy.name))
|
|
2600
|
+
else:
|
|
2601
|
+
try:
|
|
2602
|
+
shutil.move(str(legacy), str(canonical))
|
|
2603
|
+
except Exception as exc:
|
|
2604
|
+
_log(f"[F0.6 shim] move failed for misc flat file {legacy_name}: {exc}")
|
|
2605
|
+
continue
|
|
2606
|
+
elif legacy.exists():
|
|
2607
|
+
_log(f"[F0.6 shim] legacy misc flat path {legacy_name} is not a file; skipped")
|
|
2608
|
+
continue
|
|
2609
|
+
|
|
2610
|
+
if not canonical.exists():
|
|
2611
|
+
continue
|
|
2612
|
+
try:
|
|
2613
|
+
relative = os.path.relpath(str(canonical), str(legacy.parent))
|
|
2614
|
+
legacy.symlink_to(relative)
|
|
2615
|
+
except Exception as exc:
|
|
2616
|
+
_log(f"[F0.6 shim] misc file symlink create failed for {legacy_name}: {exc}")
|
|
2617
|
+
|
|
2557
2618
|
marker = NEXO_HOME / ".structure-version"
|
|
2558
2619
|
try:
|
|
2559
2620
|
marker.write_text("F0.6\n", encoding="utf-8")
|
|
@@ -2613,6 +2674,52 @@ def _cleanup_f06_root_residue() -> None:
|
|
|
2613
2674
|
_log(f"[F0.6] residue cleanup skipped for {target}: {exc}")
|
|
2614
2675
|
|
|
2615
2676
|
|
|
2677
|
+
def _heal_misplaced_personal_watchdog_runtime_files() -> list[str]:
|
|
2678
|
+
"""Move/remove watchdog runtime artifacts that ended up in personal/scripts.
|
|
2679
|
+
|
|
2680
|
+
These files are runtime state for the core watchdog and must live under
|
|
2681
|
+
``core/scripts``. Older migrations or manual copies could leave stale
|
|
2682
|
+
duplicates under ``personal/scripts``; that pollutes the personal surface
|
|
2683
|
+
and makes clean installs look dirtier than they are.
|
|
2684
|
+
"""
|
|
2685
|
+
actions: list[str] = []
|
|
2686
|
+
personal_scripts = paths.personal_scripts_dir()
|
|
2687
|
+
core_scripts = paths.core_scripts_dir()
|
|
2688
|
+
try:
|
|
2689
|
+
core_scripts.mkdir(parents=True, exist_ok=True)
|
|
2690
|
+
except Exception:
|
|
2691
|
+
return actions
|
|
2692
|
+
|
|
2693
|
+
for name in sorted(_CORE_SCRIPT_RUNTIME_FILES):
|
|
2694
|
+
personal = personal_scripts / name
|
|
2695
|
+
if not personal.exists():
|
|
2696
|
+
continue
|
|
2697
|
+
core = core_scripts / name
|
|
2698
|
+
try:
|
|
2699
|
+
if not core.exists():
|
|
2700
|
+
shutil.move(str(personal), str(core))
|
|
2701
|
+
actions.append(f"watchdog-runtime-file-moved:{name}")
|
|
2702
|
+
continue
|
|
2703
|
+
if name == ".watchdog-nexo-repair.lock":
|
|
2704
|
+
personal.unlink(missing_ok=True)
|
|
2705
|
+
actions.append(f"watchdog-runtime-file-pruned:{name}")
|
|
2706
|
+
continue
|
|
2707
|
+
try:
|
|
2708
|
+
personal_mtime = personal.stat().st_mtime
|
|
2709
|
+
core_mtime = core.stat().st_mtime
|
|
2710
|
+
except OSError:
|
|
2711
|
+
personal_mtime = core_mtime = 0
|
|
2712
|
+
if personal_mtime > core_mtime:
|
|
2713
|
+
shutil.move(str(personal), str(core))
|
|
2714
|
+
actions.append(f"watchdog-runtime-file-promoted:{name}")
|
|
2715
|
+
else:
|
|
2716
|
+
personal.unlink(missing_ok=True)
|
|
2717
|
+
actions.append(f"watchdog-runtime-file-pruned:{name}")
|
|
2718
|
+
except Exception as exc:
|
|
2719
|
+
actions.append(f"watchdog-runtime-file-warning:{name}:{exc.__class__.__name__}")
|
|
2720
|
+
return actions
|
|
2721
|
+
|
|
2722
|
+
|
|
2616
2723
|
def _maybe_migrate_to_f06_layout() -> None:
|
|
2617
2724
|
"""Plan F0.6 — one-shot physical layout migration. Idempotent.
|
|
2618
2725
|
|
|
@@ -2674,8 +2781,9 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
2674
2781
|
for sub in ("core/scripts", "core-dev/scripts", "personal/scripts",
|
|
2675
2782
|
"core/db", "core/cognitive", "core/doctor", "core/dashboard", "core/skills",
|
|
2676
2783
|
"personal/brain", "personal/skills", "personal/config",
|
|
2784
|
+
"personal/lib", "personal/lib/generated",
|
|
2677
2785
|
"core/plugins", "core/hooks", "core/rules",
|
|
2678
|
-
"runtime/data", "runtime/logs", "runtime/operations",
|
|
2786
|
+
"runtime/data", "runtime/logs", "runtime/operations", "runtime/state",
|
|
2679
2787
|
"runtime/backups", "runtime/memory",
|
|
2680
2788
|
"runtime/coordination", "runtime/exports",
|
|
2681
2789
|
"runtime/nexo-email",
|
|
@@ -2744,6 +2852,7 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
2744
2852
|
("data", NEXO_HOME / "runtime" / "data"),
|
|
2745
2853
|
("logs", NEXO_HOME / "runtime" / "logs"),
|
|
2746
2854
|
("operations", NEXO_HOME / "runtime" / "operations"),
|
|
2855
|
+
("state", NEXO_HOME / "runtime" / "state"),
|
|
2747
2856
|
("backups", NEXO_HOME / "runtime" / "backups"),
|
|
2748
2857
|
("memory", NEXO_HOME / "runtime" / "memory"),
|
|
2749
2858
|
("cognitive", paths.core_cognitive_dir(allow_legacy_fallback=False)),
|
|
@@ -2757,6 +2866,8 @@ def _maybe_migrate_to_f06_layout() -> None:
|
|
|
2757
2866
|
("plugins", NEXO_HOME / "core" / "plugins"),
|
|
2758
2867
|
("hooks", NEXO_HOME / "core" / "hooks"),
|
|
2759
2868
|
("rules", NEXO_HOME / "core" / "rules"),
|
|
2869
|
+
("workdir", paths.personal_lib_dir() / "workdir"),
|
|
2870
|
+
("working", paths.personal_lib_dir() / "working"),
|
|
2760
2871
|
]
|
|
2761
2872
|
for legacy_name, new_dir in TREE_MAP:
|
|
2762
2873
|
legacy = NEXO_HOME / legacy_name
|
|
@@ -4419,6 +4530,14 @@ def _run_runtime_post_sync(dest: Path = NEXO_HOME, progress_fn=None) -> tuple[bo
|
|
|
4419
4530
|
except Exception as exc:
|
|
4420
4531
|
actions.append(f"deep-sleep-heal-warning:{exc.__class__.__name__}")
|
|
4421
4532
|
|
|
4533
|
+
try:
|
|
4534
|
+
_emit_progress(progress_fn, "Cleaning misplaced watchdog runtime files...")
|
|
4535
|
+
misplaced_actions = _heal_misplaced_personal_watchdog_runtime_files()
|
|
4536
|
+
for action in misplaced_actions:
|
|
4537
|
+
actions.append(action)
|
|
4538
|
+
except Exception as exc:
|
|
4539
|
+
actions.append(f"watchdog-runtime-file-warning:{exc.__class__.__name__}")
|
|
4540
|
+
|
|
4422
4541
|
# Recover the user's legacy reasoning_effort preference into the new
|
|
4423
4542
|
# default_resonance knob. v5.10.0 left this gap — users who had
|
|
4424
4543
|
# `reasoning_effort="max"` in schedule.json silently degraded to
|
|
@@ -732,11 +732,12 @@ def get_operator_profile() -> dict[str, Any]:
|
|
|
732
732
|
operator_accounts: list[dict] = []
|
|
733
733
|
|
|
734
734
|
try:
|
|
735
|
+
from calibration_runtime import load_runtime_calibration
|
|
735
736
|
from paths import brain_dir
|
|
736
737
|
|
|
737
738
|
cal_path = brain_dir() / "calibration.json"
|
|
738
739
|
if cal_path.exists():
|
|
739
|
-
payload =
|
|
740
|
+
payload = load_runtime_calibration(cal_path)
|
|
740
741
|
user = payload.get("user") if isinstance(payload.get("user"), dict) else {}
|
|
741
742
|
operator_name = (
|
|
742
743
|
str(user.get("name") or "").strip()
|
package/src/autonomy_mandate.py
CHANGED
|
@@ -33,9 +33,12 @@ from datetime import date, datetime, timezone
|
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
from typing import Optional
|
|
35
35
|
|
|
36
|
+
from core_prompts import render_core_prompt
|
|
37
|
+
|
|
36
38
|
|
|
37
39
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
38
40
|
STATE_PATH = NEXO_HOME / "runtime" / "data" / "autonomy_mandate.json"
|
|
41
|
+
CLASSIFIER_QUESTION = render_core_prompt("autonomy-mandate-question")
|
|
39
42
|
|
|
40
43
|
# Marker list per NF-DS-45569A27. Case-insensitive substring match.
|
|
41
44
|
MARKERS = (
|
|
@@ -48,6 +51,7 @@ MARKERS = (
|
|
|
48
51
|
"llevo 3 veces",
|
|
49
52
|
"lo quiero ya",
|
|
50
53
|
)
|
|
54
|
+
_SEMANTIC_MARKER = "semantic-autonomy-mandate"
|
|
51
55
|
|
|
52
56
|
# Default mandate TTL. Longer than a normal working session but short enough
|
|
53
57
|
# that a stale state does not block followups weeks later.
|
|
@@ -93,13 +97,23 @@ class MandateState:
|
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
|
|
96
|
-
def _detect_marker(text: str) -> Optional[str]:
|
|
100
|
+
def _detect_marker(text: str, *, classifier=None) -> Optional[str]:
|
|
97
101
|
if not text:
|
|
98
102
|
return None
|
|
99
103
|
lowered = text.lower()
|
|
100
104
|
for marker in MARKERS:
|
|
101
105
|
if marker in lowered:
|
|
102
106
|
return marker
|
|
107
|
+
if classifier is None:
|
|
108
|
+
try:
|
|
109
|
+
from enforcement_classifier import classify as classifier # type: ignore
|
|
110
|
+
except Exception:
|
|
111
|
+
return None
|
|
112
|
+
try:
|
|
113
|
+
if bool(classifier(question=CLASSIFIER_QUESTION, context=text.strip()[:1200])):
|
|
114
|
+
return _SEMANTIC_MARKER
|
|
115
|
+
except Exception:
|
|
116
|
+
return None
|
|
103
117
|
return None
|
|
104
118
|
|
|
105
119
|
|
|
@@ -163,6 +177,7 @@ def maybe_ingest_from_text(
|
|
|
163
177
|
session_id: str,
|
|
164
178
|
source: str = "auto",
|
|
165
179
|
ttl_seconds: int = DEFAULT_TTL_SECONDS,
|
|
180
|
+
classifier=None,
|
|
166
181
|
) -> Optional[MandateState]:
|
|
167
182
|
"""Scan free-form text for a mandate marker and persist if found.
|
|
168
183
|
|
|
@@ -170,7 +185,7 @@ def maybe_ingest_from_text(
|
|
|
170
185
|
mandate can be set transparently, without a separate explicit tool.
|
|
171
186
|
Returns the new state when a marker was detected, otherwise None.
|
|
172
187
|
"""
|
|
173
|
-
marker = _detect_marker(text or "")
|
|
188
|
+
marker = _detect_marker(text or "", classifier=classifier)
|
|
174
189
|
if not marker:
|
|
175
190
|
return None
|
|
176
191
|
return set_mandate(
|
package/src/bootstrap_docs.py
CHANGED
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
import re
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from calibration_runtime import load_runtime_calibration
|
|
10
11
|
from client_preferences import (
|
|
11
12
|
BACKEND_NONE,
|
|
12
13
|
CLIENT_CLAUDE_CODE,
|
|
@@ -82,7 +83,7 @@ def _resolve_operator_name(nexo_home: Path, explicit: str = "") -> str:
|
|
|
82
83
|
env_name = os.environ.get("NEXO_NAME", "").strip()
|
|
83
84
|
if env_name:
|
|
84
85
|
return env_name
|
|
85
|
-
calibration =
|
|
86
|
+
calibration = load_runtime_calibration(nexo_home / "personal" / "brain" / "calibration.json")
|
|
86
87
|
user_payload = calibration.get("user")
|
|
87
88
|
if isinstance(user_payload, dict):
|
|
88
89
|
candidate = str(user_payload.get("assistant_name", "")).strip()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Runtime-safe calibration view.
|
|
2
|
+
|
|
3
|
+
Reads the full on-disk ``calibration.json`` but returns only the subset
|
|
4
|
+
that runtime consumers actually need for identity, preferences, and
|
|
5
|
+
profile decisions. Heavy historical arrays stay on disk untouched.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from paths import brain_dir
|
|
15
|
+
|
|
16
|
+
_TOP_LEVEL_RUNTIME_KEYS = {
|
|
17
|
+
"user",
|
|
18
|
+
"preferences",
|
|
19
|
+
"meta",
|
|
20
|
+
"assistant_name",
|
|
21
|
+
"operator_name",
|
|
22
|
+
"identity",
|
|
23
|
+
"language",
|
|
24
|
+
"lang",
|
|
25
|
+
"user_name",
|
|
26
|
+
"name",
|
|
27
|
+
"version",
|
|
28
|
+
"created",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_runtime_calibration(path: Path | None = None) -> dict:
|
|
33
|
+
target = path or (brain_dir() / "calibration.json")
|
|
34
|
+
if not target.is_file():
|
|
35
|
+
return {}
|
|
36
|
+
try:
|
|
37
|
+
payload = json.loads(target.read_text())
|
|
38
|
+
except Exception:
|
|
39
|
+
return {}
|
|
40
|
+
if not isinstance(payload, dict):
|
|
41
|
+
return {}
|
|
42
|
+
view: dict = {}
|
|
43
|
+
for key in _TOP_LEVEL_RUNTIME_KEYS:
|
|
44
|
+
if key in payload:
|
|
45
|
+
view[key] = copy.deepcopy(payload[key])
|
|
46
|
+
return view
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Shared Claude CLI resolution helpers.
|
|
4
|
+
|
|
5
|
+
Desktop-managed installs must never silently escape to a global `claude`
|
|
6
|
+
binary from PATH. This helper centralises that contract so automation
|
|
7
|
+
surfaces can share the same resolution policy.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _user_home(user_home: Path | None = None) -> Path:
|
|
17
|
+
if user_home is not None:
|
|
18
|
+
return Path(user_home).expanduser()
|
|
19
|
+
return Path(os.environ.get("HOME", str(Path.home()))).expanduser()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _desktop_install_markers(home: Path, *, include_global_markers: bool) -> list[Path]:
|
|
23
|
+
markers: list[Path] = [
|
|
24
|
+
home / "Applications" / "NEXO Desktop.app",
|
|
25
|
+
home / "Library" / "Application Support" / "NEXO Desktop",
|
|
26
|
+
home / ".local" / "share" / "NEXO Desktop",
|
|
27
|
+
home / ".config" / "NEXO Desktop",
|
|
28
|
+
]
|
|
29
|
+
# Treat the global app bundle path as a stable install marker even when
|
|
30
|
+
# tests run on non-macOS CI hosts. Explicit homes still suppress it.
|
|
31
|
+
if include_global_markers:
|
|
32
|
+
markers.insert(0, Path("/Applications/NEXO Desktop.app"))
|
|
33
|
+
if os.name == "nt":
|
|
34
|
+
local = Path(os.environ.get("LOCALAPPDATA", str(home / "AppData" / "Local")))
|
|
35
|
+
roaming = Path(os.environ.get("APPDATA", str(home / "AppData" / "Roaming")))
|
|
36
|
+
markers.extend(
|
|
37
|
+
[
|
|
38
|
+
local / "Programs" / "NEXO Desktop",
|
|
39
|
+
roaming / "NEXO Desktop",
|
|
40
|
+
]
|
|
41
|
+
)
|
|
42
|
+
return markers
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def desktop_product_requested(user_home: Path | None = None) -> bool:
|
|
46
|
+
if str(os.environ.get("NEXO_DESKTOP_MANAGED", "")).strip() == "1":
|
|
47
|
+
return True
|
|
48
|
+
explicit_home = user_home is not None
|
|
49
|
+
home = _user_home(user_home)
|
|
50
|
+
mode_paths = (
|
|
51
|
+
home / ".nexo" / "personal" / "config" / "product-mode.json",
|
|
52
|
+
home / ".nexo" / "config" / "product-mode.json",
|
|
53
|
+
)
|
|
54
|
+
for mode_path in mode_paths:
|
|
55
|
+
try:
|
|
56
|
+
payload = json.loads(mode_path.read_text())
|
|
57
|
+
except Exception:
|
|
58
|
+
continue
|
|
59
|
+
if not isinstance(payload, dict):
|
|
60
|
+
continue
|
|
61
|
+
if payload.get("desktop_managed") is True:
|
|
62
|
+
return True
|
|
63
|
+
if str(payload.get("product_mode") or "").strip().lower() == "desktop_closed_product":
|
|
64
|
+
return True
|
|
65
|
+
for marker in _desktop_install_markers(home, include_global_markers=not explicit_home):
|
|
66
|
+
try:
|
|
67
|
+
if marker.exists():
|
|
68
|
+
return True
|
|
69
|
+
except Exception:
|
|
70
|
+
continue
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def managed_claude_prefix(user_home: Path | None = None) -> Path:
|
|
75
|
+
explicit = str(os.environ.get("NEXO_CLAUDE_PREFIX", "")).strip()
|
|
76
|
+
if explicit:
|
|
77
|
+
return Path(explicit).expanduser()
|
|
78
|
+
return _user_home(user_home) / ".nexo" / "runtime" / "bootstrap" / "npm-global"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _path_within(candidate: Path, parent: Path) -> bool:
|
|
82
|
+
try:
|
|
83
|
+
candidate.expanduser().resolve(strict=False).relative_to(parent.expanduser().resolve(strict=False))
|
|
84
|
+
return True
|
|
85
|
+
except Exception:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def managed_claude_binary(user_home: Path | None = None) -> str:
|
|
90
|
+
home = _user_home(user_home)
|
|
91
|
+
managed_prefix = managed_claude_prefix(home)
|
|
92
|
+
persisted_paths = (
|
|
93
|
+
home / ".nexo" / "config" / "claude-cli-path",
|
|
94
|
+
home / ".nexo" / "personal" / "config" / "claude-cli-path",
|
|
95
|
+
)
|
|
96
|
+
candidates: list[Path] = []
|
|
97
|
+
for persisted in persisted_paths:
|
|
98
|
+
try:
|
|
99
|
+
raw = persisted.read_text(encoding="utf-8").strip()
|
|
100
|
+
except Exception:
|
|
101
|
+
raw = ""
|
|
102
|
+
if raw:
|
|
103
|
+
candidates.append(Path(raw))
|
|
104
|
+
env_path = str(os.environ.get("CLAUDE_BIN", "")).strip()
|
|
105
|
+
if env_path:
|
|
106
|
+
candidates.append(Path(env_path))
|
|
107
|
+
candidates.append(managed_prefix / "bin" / "claude")
|
|
108
|
+
for candidate in candidates:
|
|
109
|
+
try:
|
|
110
|
+
if not candidate.exists():
|
|
111
|
+
continue
|
|
112
|
+
except Exception:
|
|
113
|
+
continue
|
|
114
|
+
if _path_within(candidate, managed_prefix):
|
|
115
|
+
return str(candidate)
|
|
116
|
+
return ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def resolve_claude_cli(user_home: Path | None = None) -> str:
|
|
120
|
+
home = _user_home(user_home)
|
|
121
|
+
if desktop_product_requested(home):
|
|
122
|
+
return managed_claude_binary(home)
|
|
123
|
+
|
|
124
|
+
persisted_paths = (
|
|
125
|
+
home / ".nexo" / "config" / "claude-cli-path",
|
|
126
|
+
home / ".nexo" / "personal" / "config" / "claude-cli-path",
|
|
127
|
+
)
|
|
128
|
+
for persisted in persisted_paths:
|
|
129
|
+
try:
|
|
130
|
+
candidate = persisted.read_text(encoding="utf-8").strip()
|
|
131
|
+
except Exception:
|
|
132
|
+
candidate = ""
|
|
133
|
+
if candidate and Path(candidate).exists():
|
|
134
|
+
return candidate
|
|
135
|
+
|
|
136
|
+
env_path = str(os.environ.get("CLAUDE_BIN", "")).strip()
|
|
137
|
+
if env_path and Path(env_path).exists():
|
|
138
|
+
return env_path
|
|
139
|
+
|
|
140
|
+
discovered = shutil.which("claude")
|
|
141
|
+
if discovered:
|
|
142
|
+
return discovered
|
|
143
|
+
|
|
144
|
+
for candidate in (
|
|
145
|
+
home / ".local" / "bin" / "claude",
|
|
146
|
+
home / ".npm-global" / "bin" / "claude",
|
|
147
|
+
Path("/usr/local/bin/claude"),
|
|
148
|
+
):
|
|
149
|
+
if candidate.exists():
|
|
150
|
+
return str(candidate)
|
|
151
|
+
return ""
|