nexo-brain 7.1.0 → 7.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -2
- package/bin/nexo-brain.js +198 -92
- package/package.json +1 -1
- package/src/agent_runner.py +10 -8
- package/src/auto_close_sessions.py +19 -2
- package/src/auto_update.py +305 -42
- package/src/autonomy_mandate.py +260 -0
- package/src/bootstrap_docs.py +22 -1
- package/src/cli.py +181 -1
- package/src/cli_email.py +104 -73
- package/src/client_sync.py +22 -1
- package/src/cognitive/_core.py +5 -3
- package/src/core_prompts.py +50 -0
- package/src/cron_recovery.py +81 -7
- package/src/crons/manifest.json +57 -0
- package/src/crons/sync.py +95 -26
- package/src/dashboard/app.py +59 -0
- package/src/dashboard/templates/base.html +2 -0
- package/src/dashboard/templates/feature-disabled.html +27 -0
- package/src/db/_email_accounts.py +67 -18
- package/src/db/_fts.py +5 -5
- package/src/db/_personal_scripts.py +1 -1
- package/src/db/_skills.py +3 -3
- package/src/doctor/providers/runtime.py +35 -20
- package/src/email_config.py +18 -9
- package/src/enforcement_classifier.py +3 -12
- package/src/evolution_cycle.py +37 -149
- package/src/guardian_telemetry.py +3 -2
- package/src/hook_guardrails.py +61 -0
- package/src/hooks/capture-tool-logs.sh +11 -3
- package/src/hooks/daily-briefing-check.sh +7 -2
- package/src/hooks/heartbeat-enforcement.py +14 -1
- package/src/hooks/heartbeat-posttool.sh +2 -0
- package/src/hooks/heartbeat-user-msg.sh +2 -0
- package/src/hooks/inbox-hook.sh +6 -2
- package/src/hooks/post-compact.sh +12 -4
- package/src/hooks/pre-compact.sh +12 -4
- package/src/migrate_embeddings.py +5 -3
- package/src/nexo_migrate.py +3 -1
- package/src/plugin_loader.py +14 -5
- package/src/plugins/adaptive_mode.py +4 -1
- package/src/plugins/backup.py +32 -20
- package/src/plugins/evolution.py +2 -0
- package/src/plugins/memory_export.py +6 -1
- package/src/plugins/personal_plugins.py +17 -7
- package/src/plugins/personal_scripts.py +64 -3
- package/src/presets/entities_universal.json +67 -4
- package/src/product_mode.py +201 -0
- package/src/r14_correction_learning.py +5 -20
- package/src/r15_project_context.py +4 -10
- package/src/r16_declared_done.py +3 -16
- package/src/r17_promise_debt.py +3 -16
- package/src/r18_followup_autocomplete.py +5 -7
- package/src/r19_project_grep.py +5 -8
- package/src/r20_constant_change.py +5 -15
- package/src/r21_legacy_path.py +5 -7
- package/src/r22_personal_script.py +4 -8
- package/src/r23_ssh_without_atlas.py +4 -11
- package/src/r23b_deploy_vhost.py +7 -6
- package/src/r23c_cwd_mismatch.py +7 -6
- package/src/r23d_chown_chmod_recursive.py +6 -6
- package/src/r23e_force_push_main.py +5 -6
- package/src/r23f_db_no_where.py +5 -6
- package/src/r23g_secrets_in_output.py +5 -5
- package/src/r23h_shebang_mismatch.py +6 -5
- package/src/r23i_auto_deploy_ignored.py +5 -6
- package/src/r23j_global_install.py +5 -6
- package/src/r23k_script_duplicates_skill.py +7 -6
- package/src/r23l_resource_collision.py +7 -6
- package/src/r23m_message_duplicate.py +6 -5
- package/src/r24_stale_memory.py +4 -9
- package/src/r25_nora_maria_read_only.py +5 -10
- package/src/r34_identity_coherence.py +6 -13
- package/src/r_catalog.py +3 -7
- package/src/resonance_map.py +13 -13
- package/src/runtime_power.py +29 -80
- package/src/script_registry.py +236 -6
- package/src/scripts/check-context.py +8 -25
- package/src/scripts/deep-sleep/extract.py +6 -10
- package/src/scripts/nexo-auto-update.py +27 -4
- package/src/scripts/nexo-catchup.py +9 -19
- package/src/scripts/nexo-cognitive-decay.py +26 -3
- package/src/scripts/nexo-daily-self-audit.py +50 -51
- package/src/scripts/nexo-email-migrate-config.py +30 -11
- package/src/scripts/nexo-email-monitor.py +97 -238
- package/src/scripts/nexo-followup-runner.py +70 -133
- package/src/scripts/nexo-hook-record.py +1 -1
- package/src/scripts/nexo-immune.py +6 -31
- package/src/scripts/nexo-impact-scorer.py +27 -4
- package/src/scripts/nexo-learning-housekeep.py +26 -3
- package/src/scripts/nexo-learning-validator.py +34 -32
- package/src/scripts/nexo-migrate.py +28 -12
- package/src/scripts/nexo-morning-agent.py +9 -23
- package/src/scripts/nexo-outcome-checker.py +27 -4
- package/src/scripts/nexo-postmortem-consolidator.py +30 -62
- package/src/scripts/nexo-pre-commit.py +28 -0
- package/src/scripts/nexo-proactive-dashboard.py +27 -0
- package/src/scripts/nexo-reflection.py +33 -3
- package/src/scripts/nexo-runtime-preflight.py +27 -2
- package/src/scripts/nexo-send-reply.py +10 -8
- package/src/scripts/nexo-sleep.py +11 -25
- package/src/scripts/nexo-synthesis.py +7 -40
- package/src/scripts/nexo-watchdog-smoke.py +30 -1
- package/src/scripts/nexo-watchdog.sh +23 -17
- package/src/scripts/phase_guardian_analysis.py +27 -4
- package/src/server.py +14 -3
- package/src/storage_router.py +8 -6
- package/src/tools_drive.py +5 -13
- package/src/tools_guardian.py +3 -4
- package/src/tools_menu.py +2 -2
- package/src/tools_reminders_crud.py +17 -0
- package/src/tools_sessions.py +1 -4
- package/src/user_context.py +3 -6
- package/src/user_data_portability.py +31 -23
- package/templates/CLAUDE.md.template +11 -3
- package/templates/CODEX.AGENTS.md.template +11 -3
- package/templates/core-prompts/catchup-assessment.md +19 -0
- package/templates/core-prompts/check-context.md +24 -0
- package/templates/core-prompts/daily-self-audit.md +42 -0
- package/templates/core-prompts/daily-synthesis.md +40 -0
- package/templates/core-prompts/deep-sleep-extract-json-output.md +8 -0
- package/templates/core-prompts/drive-signal-classifier-system.md +4 -0
- package/templates/core-prompts/drive-signal-classifier-user.md +6 -0
- package/templates/core-prompts/email-monitor.md +202 -0
- package/templates/core-prompts/enforcement-classifier-retry.md +1 -0
- package/templates/core-prompts/enforcement-classifier-strict.md +1 -0
- package/templates/core-prompts/evolution-public-contribution.md +32 -0
- package/templates/core-prompts/evolution-public-pr-review.md +38 -0
- package/templates/core-prompts/evolution-weekly.md +71 -0
- package/templates/core-prompts/followup-runner-operator-attention-context.md +4 -0
- package/templates/core-prompts/followup-runner-operator-attention-question.md +1 -0
- package/templates/core-prompts/followup-runner.md +74 -0
- package/templates/core-prompts/immune-triage.md +31 -0
- package/templates/core-prompts/interactive-startup.md +1 -0
- package/templates/core-prompts/json-object-only.md +1 -0
- package/templates/core-prompts/learning-validator.md +25 -0
- package/templates/core-prompts/morning-agent-json-output.md +1 -0
- package/templates/core-prompts/morning-agent.md +23 -0
- package/templates/core-prompts/postmortem-consolidator.md +60 -0
- package/templates/core-prompts/r-catalog.md +1 -0
- package/templates/core-prompts/r14-correction-learning-injection.md +1 -0
- package/templates/core-prompts/r14-correction-learning-question.md +1 -0
- package/templates/core-prompts/r15-project-context-injection.md +1 -0
- package/templates/core-prompts/r16-declared-done-injection.md +1 -0
- package/templates/core-prompts/r16-declared-done-question.md +1 -0
- package/templates/core-prompts/r17-promise-debt-injection.md +1 -0
- package/templates/core-prompts/r17-promise-debt-question.md +1 -0
- package/templates/core-prompts/r18-followup-autocomplete-injection.md +3 -0
- package/templates/core-prompts/r19-project-grep-injection.md +1 -0
- package/templates/core-prompts/r20-constant-change-injection.md +1 -0
- package/templates/core-prompts/r20-constant-change-question.md +1 -0
- package/templates/core-prompts/r21-legacy-path-injection.md +1 -0
- package/templates/core-prompts/r22-personal-script-injection.md +1 -0
- package/templates/core-prompts/r23-ssh-without-atlas-injection.md +1 -0
- package/templates/core-prompts/r23b-deploy-vhost-injection.md +1 -0
- package/templates/core-prompts/r23c-cwd-mismatch-injection.md +1 -0
- package/templates/core-prompts/r23d-chown-chmod-recursive-injection.md +1 -0
- package/templates/core-prompts/r23e-force-push-main-injection.md +1 -0
- package/templates/core-prompts/r23f-db-no-where-injection.md +1 -0
- package/templates/core-prompts/r23g-secrets-in-output-injection.md +1 -0
- package/templates/core-prompts/r23h-shebang-mismatch-injection.md +1 -0
- package/templates/core-prompts/r23i-auto-deploy-ignored-injection.md +1 -0
- package/templates/core-prompts/r23j-global-install-injection.md +1 -0
- package/templates/core-prompts/r23k-script-duplicates-skill-injection.md +1 -0
- package/templates/core-prompts/r23l-resource-collision-injection.md +1 -0
- package/templates/core-prompts/r23m-message-duplicate-injection.md +1 -0
- package/templates/core-prompts/r24-stale-memory-injection.md +1 -0
- package/templates/core-prompts/r25-read-only-host-injection.md +1 -0
- package/templates/core-prompts/r34-identity-coherence-probe.md +1 -0
- package/templates/core-prompts/r34-identity-coherence-question.md +1 -0
- package/templates/core-prompts/sleep.md +25 -0
- package/templates/email-template.md +55 -0
- package/templates/nexo_helper.py +31 -13
- package/templates/plugin-template.py +3 -3
- package/templates/skill-template.md +2 -1
|
@@ -25,7 +25,7 @@ from client_preferences import (
|
|
|
25
25
|
normalize_client_preferences,
|
|
26
26
|
resolve_client_runtime_profile,
|
|
27
27
|
)
|
|
28
|
-
from cron_recovery import resolve_declared_schedule, should_run_at_load
|
|
28
|
+
from cron_recovery import is_cron_enabled, resolve_declared_schedule, should_run_at_load
|
|
29
29
|
from doctor.models import DoctorCheck, safe_check
|
|
30
30
|
|
|
31
31
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
@@ -43,15 +43,20 @@ IMMUNE_FRESHNESS = 3600 # 1 hour (runs every 30 min)
|
|
|
43
43
|
WATCHDOG_FRESHNESS = 3600 # 1 hour (runs every 30 min)
|
|
44
44
|
DEFAULT_CRON_THRESHOLD = 7200 # Fallback when manifest data is unavailable
|
|
45
45
|
LIVE_PROTOCOL_SESSION_FRESHNESS = 1800 # 30 minutes
|
|
46
|
-
|
|
47
|
-
SPECIAL_LAUNCHAGENT_IDS = {"prevent-sleep", "tcc-approve"}
|
|
48
|
-
SPECIAL_ENV_NORMALIZE_IDS = SPECIAL_LAUNCHAGENT_IDS
|
|
46
|
+
SPECIAL_ENV_NORMALIZE_IDS = {"prevent-sleep", "tcc-approve"}
|
|
49
47
|
OPTIONALS_FILE = paths.config_dir() / "optionals.json"
|
|
50
48
|
SCHEDULE_FILE = paths.config_dir() / "schedule.json"
|
|
51
49
|
PACKAGE_JSON = NEXO_CODE / "package.json"
|
|
52
50
|
CHANGELOG_FILE = NEXO_CODE / "CHANGELOG.md"
|
|
53
51
|
|
|
54
52
|
|
|
53
|
+
def _expected_runtime_code_dir() -> Path:
|
|
54
|
+
packaged = NEXO_HOME / "core"
|
|
55
|
+
if packaged.exists() or not (NEXO_HOME / "server.py").is_file():
|
|
56
|
+
return packaged
|
|
57
|
+
return NEXO_HOME
|
|
58
|
+
|
|
59
|
+
|
|
55
60
|
def _recorded_source_root() -> Path | None:
|
|
56
61
|
version_file = NEXO_HOME / "version.json"
|
|
57
62
|
try:
|
|
@@ -832,15 +837,16 @@ def _enabled_optionals() -> dict[str, bool]:
|
|
|
832
837
|
def _enabled_manifest_crons() -> list[dict]:
|
|
833
838
|
manifest_candidates = [
|
|
834
839
|
paths.crons_dir() / "manifest.json",
|
|
840
|
+
NEXO_HOME / "crons" / "manifest.json",
|
|
835
841
|
NEXO_CODE / "crons" / "manifest.json",
|
|
836
842
|
]
|
|
837
843
|
optionals = _enabled_optionals()
|
|
838
|
-
|
|
844
|
+
schedule = {}
|
|
839
845
|
try:
|
|
840
846
|
if SCHEDULE_FILE.is_file():
|
|
841
|
-
|
|
842
|
-
if isinstance(
|
|
843
|
-
|
|
847
|
+
loaded = _load_json(SCHEDULE_FILE)
|
|
848
|
+
if isinstance(loaded, dict):
|
|
849
|
+
schedule = loaded
|
|
844
850
|
except Exception:
|
|
845
851
|
pass
|
|
846
852
|
for manifest_path in manifest_candidates:
|
|
@@ -856,12 +862,12 @@ def _enabled_manifest_crons() -> list[dict]:
|
|
|
856
862
|
cron_id = cron.get("id")
|
|
857
863
|
if not cron_id:
|
|
858
864
|
continue
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
+
if not is_cron_enabled(
|
|
866
|
+
cron,
|
|
867
|
+
optionals=optionals,
|
|
868
|
+
schedule_data=schedule,
|
|
869
|
+
system=os.environ.get("NEXO_PLATFORM") or platform.system(),
|
|
870
|
+
):
|
|
865
871
|
continue
|
|
866
872
|
enabled.append(cron)
|
|
867
873
|
return enabled
|
|
@@ -919,6 +925,7 @@ def _launchagent_schedule_expectations() -> dict[str, dict]:
|
|
|
919
925
|
"StartCalendarInterval": None,
|
|
920
926
|
"RunAtLoad": None,
|
|
921
927
|
"KeepAlive": None,
|
|
928
|
+
"WatchPaths": None,
|
|
922
929
|
"schedule_configured": False,
|
|
923
930
|
}
|
|
924
931
|
if cron.get("keep_alive"):
|
|
@@ -944,12 +951,17 @@ def _launchagent_schedule_expectations() -> dict[str, dict]:
|
|
|
944
951
|
elif should_run_at_load(cron):
|
|
945
952
|
expected["RunAtLoad"] = True
|
|
946
953
|
expected["schedule_configured"] = True
|
|
954
|
+
if cron.get("watch_paths"):
|
|
955
|
+
expected["WatchPaths"] = [
|
|
956
|
+
str(Path(str(path)).expanduser()) if str(path).startswith("~") else str(path)
|
|
957
|
+
for path in cron.get("watch_paths", [])
|
|
958
|
+
]
|
|
947
959
|
expectations[cron_id] = expected
|
|
948
960
|
return expectations
|
|
949
961
|
|
|
950
962
|
|
|
951
963
|
def _managed_launchagent_plists() -> list[tuple[str, Path]]:
|
|
952
|
-
ids = set(
|
|
964
|
+
ids = set()
|
|
953
965
|
for cron_id, expected in _launchagent_schedule_expectations().items():
|
|
954
966
|
if expected.get("schedule_configured"):
|
|
955
967
|
ids.add(cron_id)
|
|
@@ -963,7 +975,7 @@ def _managed_launchagent_plists() -> list[tuple[str, Path]]:
|
|
|
963
975
|
|
|
964
976
|
|
|
965
977
|
def _known_nexo_launchagent_ids() -> set[str]:
|
|
966
|
-
ids = set(
|
|
978
|
+
ids = set()
|
|
967
979
|
for cron_id, expected in _launchagent_schedule_expectations().items():
|
|
968
980
|
if expected.get("schedule_configured"):
|
|
969
981
|
ids.add(cron_id)
|
|
@@ -1088,6 +1100,7 @@ def _repair_launchagents(items: list[tuple[str, Path]]) -> tuple[bool, list[str]
|
|
|
1088
1100
|
def _repair_special_launchagent_plists(items: list[tuple[str, Path]]) -> tuple[bool, list[str]]:
|
|
1089
1101
|
evidence: list[str] = []
|
|
1090
1102
|
ok = True
|
|
1103
|
+
expected_code = _expected_runtime_code_dir()
|
|
1091
1104
|
for cron_id, plist_path in items:
|
|
1092
1105
|
if cron_id not in SPECIAL_ENV_NORMALIZE_IDS:
|
|
1093
1106
|
continue
|
|
@@ -1096,8 +1109,8 @@ def _repair_special_launchagent_plists(items: list[tuple[str, Path]]) -> tuple[b
|
|
|
1096
1109
|
plist_data = plistlib.load(fh)
|
|
1097
1110
|
env = plist_data.setdefault("EnvironmentVariables", {})
|
|
1098
1111
|
changed = False
|
|
1099
|
-
if env.get("NEXO_CODE") != str(
|
|
1100
|
-
env["NEXO_CODE"] = str(
|
|
1112
|
+
if env.get("NEXO_CODE") != str(expected_code):
|
|
1113
|
+
env["NEXO_CODE"] = str(expected_code)
|
|
1101
1114
|
changed = True
|
|
1102
1115
|
if env.get("NEXO_HOME") != str(NEXO_HOME):
|
|
1103
1116
|
env["NEXO_HOME"] = str(NEXO_HOME)
|
|
@@ -1533,12 +1546,14 @@ def check_launchagent_integrity(fix: bool = False) -> DoctorCheck:
|
|
|
1533
1546
|
"StartCalendarInterval": plist_data.get("StartCalendarInterval"),
|
|
1534
1547
|
"RunAtLoad": plist_data.get("RunAtLoad"),
|
|
1535
1548
|
"KeepAlive": plist_data.get("KeepAlive"),
|
|
1549
|
+
"WatchPaths": plist_data.get("WatchPaths"),
|
|
1536
1550
|
}
|
|
1537
1551
|
target_schedule = {
|
|
1538
1552
|
"StartInterval": expected_schedule.get("StartInterval"),
|
|
1539
1553
|
"StartCalendarInterval": expected_schedule.get("StartCalendarInterval"),
|
|
1540
1554
|
"RunAtLoad": expected_schedule.get("RunAtLoad"),
|
|
1541
1555
|
"KeepAlive": expected_schedule.get("KeepAlive"),
|
|
1556
|
+
"WatchPaths": expected_schedule.get("WatchPaths"),
|
|
1542
1557
|
}
|
|
1543
1558
|
if actual_schedule != target_schedule:
|
|
1544
1559
|
problems.append(
|
|
@@ -1764,12 +1779,12 @@ def check_personal_script_registry(fix: bool = False) -> DoctorCheck:
|
|
|
1764
1779
|
"Run nexo scripts sync to reconcile filesystem scripts and personal LaunchAgents",
|
|
1765
1780
|
"Run nexo scripts reconcile so declared schedules are recreated through the official flow",
|
|
1766
1781
|
"Use nexo doctor --tier runtime --fix to apply the safe reconcile path for declared schedules",
|
|
1767
|
-
"Keep personal scripts in NEXO_HOME/scripts so updates do not collide with core",
|
|
1782
|
+
"Keep personal scripts in NEXO_HOME/personal/scripts so updates do not collide with core",
|
|
1768
1783
|
"Prefer ps- prefixed filenames for new personal scripts so ownership stays obvious at a glance",
|
|
1769
1784
|
],
|
|
1770
1785
|
escalation_prompt=(
|
|
1771
1786
|
"Personal script metadata, files, and personal cron schedules are out of sync. "
|
|
1772
|
-
"Reconcile NEXO_HOME/scripts with personal LaunchAgents without treating them as core crons."
|
|
1787
|
+
"Reconcile NEXO_HOME/personal/scripts with personal LaunchAgents without treating them as core crons."
|
|
1773
1788
|
),
|
|
1774
1789
|
)
|
|
1775
1790
|
|
package/src/email_config.py
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
The loader prefers the `email_accounts` table. When the table is empty
|
|
5
5
|
(fresh install that hasn't run `nexo email setup` yet) it falls back
|
|
6
6
|
to the legacy JSON for backwards compatibility — no crons stall while
|
|
7
|
-
|
|
7
|
+
the operator migrates.
|
|
8
8
|
|
|
9
9
|
Usage from any script:
|
|
10
10
|
|
|
@@ -117,6 +117,8 @@ def _account_to_legacy_shape(
|
|
|
117
117
|
"password": runtime_account["password"],
|
|
118
118
|
"operator_email": default_operator_email,
|
|
119
119
|
"operator_aliases": list(extra_operator_emails or []),
|
|
120
|
+
# Transitional compatibility for older scripts that still read the
|
|
121
|
+
# pre-F2 alias key directly.
|
|
120
122
|
"francisco_emails": list(extra_operator_emails or []),
|
|
121
123
|
"trusted_domains": runtime_account["trusted_domains"],
|
|
122
124
|
"sender_policy": runtime_account["sender_policy"],
|
|
@@ -205,19 +207,26 @@ def load_email_runtime_snapshot() -> dict[str, Any] | None:
|
|
|
205
207
|
}
|
|
206
208
|
|
|
207
209
|
|
|
208
|
-
def load_email_config(label: str | None = None) -> dict | None:
|
|
209
|
-
"""Return the email config for a given
|
|
210
|
+
def load_email_config(label: str | None = None, account_id: int | str | None = None) -> dict | None:
|
|
211
|
+
"""Return the email config for a given selector (or the primary account).
|
|
210
212
|
|
|
211
213
|
Preference order:
|
|
212
|
-
1. email_accounts table (via label or get_primary_email_account).
|
|
214
|
+
1. email_accounts table (via id, label, or get_primary_email_account).
|
|
213
215
|
2. ~/.nexo/nexo-email/config.json legacy file.
|
|
214
216
|
3. None if neither is available.
|
|
215
217
|
"""
|
|
216
218
|
account: dict | None = None
|
|
217
219
|
operator_accounts: list[dict] = []
|
|
218
220
|
try:
|
|
219
|
-
from db._email_accounts import
|
|
220
|
-
|
|
221
|
+
from db._email_accounts import (
|
|
222
|
+
get_email_account,
|
|
223
|
+
get_email_account_by_id,
|
|
224
|
+
get_primary_email_account,
|
|
225
|
+
list_email_accounts,
|
|
226
|
+
)
|
|
227
|
+
if account_id is not None:
|
|
228
|
+
account = get_email_account_by_id(account_id)
|
|
229
|
+
elif label:
|
|
221
230
|
account = get_email_account(label)
|
|
222
231
|
else:
|
|
223
232
|
account = get_primary_email_account()
|
|
@@ -231,9 +240,9 @@ def load_email_config(label: str | None = None) -> dict | None:
|
|
|
231
240
|
value = str(op.get("email") or "").strip().lower()
|
|
232
241
|
if value and value not in extra:
|
|
233
242
|
extra.append(value)
|
|
234
|
-
# F1/F2 — operator aliases are canonical now. Keep the legacy
|
|
235
|
-
#
|
|
236
|
-
#
|
|
243
|
+
# F1/F2 — operator aliases are canonical now. Keep the legacy alias
|
|
244
|
+
# compatibility key in the returned payload so old code paths do not
|
|
245
|
+
# break during transition.
|
|
237
246
|
aliases = (account.get("metadata") or {}).get("operator_aliases") or []
|
|
238
247
|
for a in aliases:
|
|
239
248
|
if a and a not in extra:
|
|
@@ -40,24 +40,15 @@ import time
|
|
|
40
40
|
from typing import Callable
|
|
41
41
|
|
|
42
42
|
from call_model_raw import ClassifierUnavailableError, call_model_raw
|
|
43
|
+
from core_prompts import render_core_prompt
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
_logger = logging.getLogger("nexo.enforcement_classifier")
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
_STRICT_SYSTEM_PROMPT = (
|
|
49
|
-
"You are a binary classifier for the NEXO Protocol Enforcer. "
|
|
50
|
-
"Respond with EXACTLY ONE WORD: yes OR no. "
|
|
51
|
-
"No explanation. No preface. No punctuation. No quotes. "
|
|
52
|
-
"Only 'yes' or 'no', lowercase, no surrounding text."
|
|
53
|
-
)
|
|
49
|
+
_STRICT_SYSTEM_PROMPT = render_core_prompt("enforcement-classifier-strict")
|
|
54
50
|
|
|
55
|
-
_RETRY_SYSTEM_PROMPT = (
|
|
56
|
-
"Your previous response was not valid. "
|
|
57
|
-
"Answer with only the single word 'yes' or the single word 'no'. "
|
|
58
|
-
"Any other output is rejected. Do not explain. Do not apologise. "
|
|
59
|
-
"Do not repeat the question. Emit 'yes' or 'no' and stop."
|
|
60
|
-
)
|
|
51
|
+
_RETRY_SYSTEM_PROMPT = render_core_prompt("enforcement-classifier-retry")
|
|
61
52
|
|
|
62
53
|
_PARSER_REGEX = re.compile(r"^\s*(yes|no)\b", flags=re.IGNORECASE)
|
|
63
54
|
|
package/src/evolution_cycle.py
CHANGED
|
@@ -14,6 +14,7 @@ import sqlite3
|
|
|
14
14
|
import time
|
|
15
15
|
from datetime import datetime, date, timedelta
|
|
16
16
|
from pathlib import Path
|
|
17
|
+
from core_prompts import render_core_prompt
|
|
17
18
|
|
|
18
19
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
19
20
|
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(NEXO_HOME)))
|
|
@@ -230,7 +231,7 @@ def dry_run_restore_test() -> bool:
|
|
|
230
231
|
|
|
231
232
|
test_file.write_text("modified_content")
|
|
232
233
|
|
|
233
|
-
# Find restore script:
|
|
234
|
+
# Find restore script: repo scripts/ first, then installed core/scripts/.
|
|
234
235
|
_nexo_code = Path(os.environ.get("NEXO_CODE", ""))
|
|
235
236
|
restore_script = None
|
|
236
237
|
for candidate in [_nexo_code / "scripts" / "nexo-snapshot-restore.sh",
|
|
@@ -290,11 +291,11 @@ def build_evolution_prompt(week_data: dict, objective: dict) -> str:
|
|
|
290
291
|
max_auto = max_auto_changes(total)
|
|
291
292
|
if mode == "review":
|
|
292
293
|
mode_desc = "review-only, nothing executes automatically"
|
|
293
|
-
safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/"
|
|
294
|
+
safe_zones = "~/.nexo/personal/scripts/, ~/.nexo/personal/plugins/, ~/.nexo/personal/brain/"
|
|
294
295
|
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, cognitive.py, knowledge_graph.py, tools_*.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md"
|
|
295
296
|
elif mode == "managed":
|
|
296
297
|
mode_desc = f"owner-managed, max {max_auto} auto-applied changes with rollback and followups"
|
|
297
|
-
safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/, ~/.nexo/brain/, NEXO_CODE/src, repo bin/docs/templates/tests"
|
|
298
|
+
safe_zones = "~/.nexo/personal/scripts/, ~/.nexo/personal/plugins/, ~/.nexo/personal/brain/, NEXO_CODE/src, repo bin/docs/templates/tests"
|
|
298
299
|
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md, personality.md, user-profile.md"
|
|
299
300
|
elif mode == "public_core":
|
|
300
301
|
mode_desc = "public core contribution via isolated checkout and Draft PR"
|
|
@@ -302,82 +303,25 @@ def build_evolution_prompt(week_data: dict, objective: dict) -> str:
|
|
|
302
303
|
immutable_files = "personal runtime, ~/.nexo/**, local DBs/logs, CLAUDE.md, AGENTS.md, user-profile.md"
|
|
303
304
|
else:
|
|
304
305
|
mode_desc = f"public auto, max {max_auto} auto-applied changes in personal safe zones"
|
|
305
|
-
safe_zones = "~/.nexo/scripts/, ~/.nexo/plugins/"
|
|
306
|
+
safe_zones = "~/.nexo/personal/scripts/, ~/.nexo/personal/plugins/"
|
|
306
307
|
immutable_files = "db.py, server.py, plugin_loader.py, storage_router.py, cognitive.py, knowledge_graph.py, tools_*.py, nexo-watchdog.sh, evolution_cycle.py, CLAUDE.md, AGENTS.md"
|
|
307
308
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
1. Bash: sqlite3 {NEXO_DB} "SELECT category, title FROM learnings WHERE created_at > {time.time() - 7*86400} ORDER BY created_at DESC LIMIT 30"
|
|
325
|
-
2. Bash: sqlite3 {NEXO_DB} "SELECT area, COUNT(*) as cnt FROM error_repetitions GROUP BY area ORDER BY cnt DESC LIMIT 10"
|
|
326
|
-
3. Read ~/.nexo/coordination/daily-synthesis.md — today's context
|
|
327
|
-
4. Read ~/.nexo/coordination/postmortem-daily.md — self-critique patterns
|
|
328
|
-
5. Read ~/.nexo/logs/self-audit-summary.json — system health
|
|
329
|
-
6. Glob ~/.nexo/scripts/*.py — existing scripts
|
|
330
|
-
7. Glob ~/.nexo/plugins/*.py — existing plugins
|
|
331
|
-
|
|
332
|
-
LOOK FOR:
|
|
333
|
-
- Repeated errors that guard isn't preventing
|
|
334
|
-
- Scripts or processes that are failing or underperforming
|
|
335
|
-
- Missing functionality that session diaries keep asking for
|
|
336
|
-
- Redundant code or config that could be simplified
|
|
337
|
-
- Patterns in self-critique that suggest systemic issues
|
|
338
|
-
|
|
339
|
-
SAFETY:
|
|
340
|
-
- Safe zones for this mode: {safe_zones}
|
|
341
|
-
- IMMUTABLE files (never touch in this mode): {immutable_files}
|
|
342
|
-
- Every change needs: what file, what to change, why, risk, how to verify
|
|
343
|
-
- AUTO changes must be deterministic. If the edit is ambiguous, risky, or needs human taste, mark it as "propose".
|
|
344
|
-
- In managed mode, failed AUTO changes will be rolled back automatically and turned into followups with evidence.
|
|
345
|
-
|
|
346
|
-
OUTPUT FORMAT (JSON):
|
|
347
|
-
{{
|
|
348
|
-
"analysis": "one paragraph summary of what you found",
|
|
349
|
-
"dimension_scores": {{
|
|
350
|
-
"episodic_memory": 0,
|
|
351
|
-
"autonomy": 0,
|
|
352
|
-
"proactivity": 0,
|
|
353
|
-
"self_improvement": 0,
|
|
354
|
-
"agi": 0
|
|
355
|
-
}},
|
|
356
|
-
"score_evidence": {{
|
|
357
|
-
"episodic_memory": "why this score changed or stayed flat",
|
|
358
|
-
"autonomy": "why this score changed or stayed flat",
|
|
359
|
-
"proactivity": "why this score changed or stayed flat",
|
|
360
|
-
"self_improvement": "why this score changed or stayed flat",
|
|
361
|
-
"agi": "why this score changed or stayed flat"
|
|
362
|
-
}},
|
|
363
|
-
"patterns": [{{"type": "...", "description": "...", "frequency": "..."}}],
|
|
364
|
-
"proposals": [
|
|
365
|
-
{{
|
|
366
|
-
"classification": "auto" or "propose",
|
|
367
|
-
"dimension": "reliability|proactivity|efficiency|safety|learning",
|
|
368
|
-
"action": "what to do",
|
|
369
|
-
"reasoning": "why",
|
|
370
|
-
"scope": "local",
|
|
371
|
-
"changes": [{{"file": "path", "operation": "create|replace|append", "search": "text to find", "content": "new text"}}]
|
|
372
|
-
}}
|
|
373
|
-
]
|
|
374
|
-
}}
|
|
375
|
-
|
|
376
|
-
Always include all five canonical keys in `dimension_scores` and `score_evidence`.
|
|
377
|
-
Scores must be integers in the 0-100 range and reflect the current week, not targets.
|
|
378
|
-
Max 3 proposals. Quality over quantity. If nothing needs improving, say so."""
|
|
379
|
-
|
|
380
|
-
return prompt
|
|
309
|
+
return render_core_prompt(
|
|
310
|
+
"evolution-weekly",
|
|
311
|
+
learnings_this_week=stats["learnings_this_week"],
|
|
312
|
+
decisions_this_week=stats["decisions_this_week"],
|
|
313
|
+
changes_this_week=stats["changes_this_week"],
|
|
314
|
+
diaries_this_week=stats["diaries_this_week"],
|
|
315
|
+
evolution_history=stats["evolution_history"],
|
|
316
|
+
current_scores_json=json.dumps(stats["current_scores"]),
|
|
317
|
+
mode=mode,
|
|
318
|
+
mode_desc=mode_desc,
|
|
319
|
+
cycle_number=total + 1,
|
|
320
|
+
nexo_db=NEXO_DB,
|
|
321
|
+
week_cutoff_ts=time.time() - 7 * 86400,
|
|
322
|
+
safe_zones=safe_zones,
|
|
323
|
+
immutable_files=immutable_files,
|
|
324
|
+
)
|
|
381
325
|
|
|
382
326
|
|
|
383
327
|
def build_public_contribution_prompt(
|
|
@@ -414,39 +358,12 @@ needs the same change and port it if necessary. If the repo is already correct,
|
|
|
414
358
|
make the smallest validating change that captures the same gap.
|
|
415
359
|
"""
|
|
416
360
|
|
|
417
|
-
return
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
STRICT RULES:
|
|
424
|
-
- Work only inside this repository checkout: {repo_root}
|
|
425
|
-
- You may modify only public core surfaces: src/, bin/, tests/, templates/, hooks/, migrations/, .claude-plugin/
|
|
426
|
-
- Do not read or use ~/.nexo, local DBs, personal scripts, emails, logs, prompts, secrets, or any user-identifying paths
|
|
427
|
-
- Do not push, open PRs, or change git remotes yourself
|
|
428
|
-
- Do not touch README, website, gh-pages, changelog, or release metadata in this mode
|
|
429
|
-
- Focus on one concrete improvement only
|
|
430
|
-
- Run validation for the files you touched
|
|
431
|
-
|
|
432
|
-
What to do:
|
|
433
|
-
1. Inspect the repo and find a real, self-contained improvement in reliability, install/update behavior, cron recovery, diagnostics, hooks, tests, or other core infrastructure.
|
|
434
|
-
2. Implement the change directly in this checkout.
|
|
435
|
-
3. Run the smallest relevant validation commands.
|
|
436
|
-
4. Return ONLY valid JSON with this shape:
|
|
437
|
-
|
|
438
|
-
{{
|
|
439
|
-
"title": "type: short title",
|
|
440
|
-
"problem": "what was wrong",
|
|
441
|
-
"summary": "what you changed",
|
|
442
|
-
"tests": ["command 1", "command 2"],
|
|
443
|
-
"risks": ["risk 1", "risk 2"]
|
|
444
|
-
}}
|
|
445
|
-
|
|
446
|
-
Cycle: #{cycle_number}
|
|
447
|
-
Quality over quantity. One strong improvement is better than three weak ones.
|
|
448
|
-
{queued_section}
|
|
449
|
-
"""
|
|
361
|
+
return render_core_prompt(
|
|
362
|
+
"evolution-public-contribution",
|
|
363
|
+
repo_root=repo_root,
|
|
364
|
+
cycle_number=cycle_number,
|
|
365
|
+
queued_section=queued_section,
|
|
366
|
+
)
|
|
450
367
|
|
|
451
368
|
|
|
452
369
|
def build_public_pr_review_prompt(
|
|
@@ -470,45 +387,16 @@ def build_public_pr_review_prompt(
|
|
|
470
387
|
if len(trimmed_diff) > 80000:
|
|
471
388
|
trimmed_diff = trimmed_diff[:80000] + "\n\n[diff truncated by NEXO]"
|
|
472
389
|
|
|
473
|
-
return
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
- URL: {url}
|
|
484
|
-
- Base the review only on the provided title, body, file list, and diff
|
|
485
|
-
- Do not assume hidden context
|
|
486
|
-
- If confidence is not strong, choose `comment`, not `approve`
|
|
487
|
-
- If the diff is too incomplete, too risky, or too ambiguous, choose `skip`
|
|
488
|
-
- Never suggest merge authority; maintainers decide that later
|
|
489
|
-
- Keep the review concise, technical, and useful
|
|
490
|
-
|
|
491
|
-
PR TITLE:
|
|
492
|
-
{title}
|
|
493
|
-
|
|
494
|
-
PR BODY:
|
|
495
|
-
{body or "(empty)"}
|
|
496
|
-
|
|
497
|
-
FILES CHANGED:
|
|
498
|
-
{rendered_files}
|
|
499
|
-
|
|
500
|
-
DIFF:
|
|
501
|
-
```diff
|
|
502
|
-
{trimmed_diff or "(empty diff)"}
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
Return ONLY valid JSON:
|
|
506
|
-
{{
|
|
507
|
-
"decision": "approve|comment|skip",
|
|
508
|
-
"summary": "one-line verdict",
|
|
509
|
-
"body": "the exact markdown text to post as the review body"
|
|
510
|
-
}}
|
|
511
|
-
"""
|
|
390
|
+
return render_core_prompt(
|
|
391
|
+
"evolution-public-pr-review",
|
|
392
|
+
pr_number=pr_number,
|
|
393
|
+
author=author,
|
|
394
|
+
url=url,
|
|
395
|
+
title=title,
|
|
396
|
+
body=body or "(empty)",
|
|
397
|
+
rendered_files=rendered_files,
|
|
398
|
+
trimmed_diff=trimmed_diff or "(empty diff)",
|
|
399
|
+
)
|
|
512
400
|
|
|
513
401
|
|
|
514
402
|
def max_auto_changes(total_evolutions: int) -> int:
|
|
@@ -29,14 +29,15 @@ import threading
|
|
|
29
29
|
import time
|
|
30
30
|
from typing import Any
|
|
31
31
|
|
|
32
|
+
from paths import logs_dir
|
|
33
|
+
|
|
32
34
|
|
|
33
35
|
DEFAULT_MAX_BYTES = 10 * 1024 * 1024 # 10 MB per file before rotation
|
|
34
36
|
_LOCK = threading.Lock()
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
def _telemetry_path() -> pathlib.Path:
|
|
38
|
-
|
|
39
|
-
return home / "logs" / "guardian-telemetry.ndjson"
|
|
40
|
+
return logs_dir() / "guardian-telemetry.ndjson"
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
def _rotate_if_needed(path: pathlib.Path, max_bytes: int) -> None:
|
package/src/hook_guardrails.py
CHANGED
|
@@ -12,6 +12,7 @@ import paths
|
|
|
12
12
|
from db import create_protocol_debt, get_db
|
|
13
13
|
from plugins.guard import _load_conditioned_learnings, _normalize_path_token
|
|
14
14
|
from protocol_settings import get_protocol_strictness
|
|
15
|
+
from product_mode import core_writes_allowed, is_protected_runtime_core_path
|
|
15
16
|
|
|
16
17
|
READ_LIKE_TOOLS = {"Read"}
|
|
17
18
|
WRITE_LIKE_TOOLS = {"Edit", "MultiEdit", "Write"}
|
|
@@ -520,6 +521,44 @@ def _collect_automation_live_repo_blocks(
|
|
|
520
521
|
return blocks
|
|
521
522
|
|
|
522
523
|
|
|
524
|
+
def _collect_runtime_core_write_blocks(
|
|
525
|
+
conn,
|
|
526
|
+
*,
|
|
527
|
+
sid: str,
|
|
528
|
+
tool_name: str,
|
|
529
|
+
files: list[str],
|
|
530
|
+
) -> list[dict]:
|
|
531
|
+
if core_writes_allowed():
|
|
532
|
+
return []
|
|
533
|
+
blocks: list[dict] = []
|
|
534
|
+
for filepath in files:
|
|
535
|
+
if not is_protected_runtime_core_path(filepath):
|
|
536
|
+
continue
|
|
537
|
+
debt = _ensure_protocol_debt(
|
|
538
|
+
conn,
|
|
539
|
+
session_id=sid,
|
|
540
|
+
task_id="",
|
|
541
|
+
debt_type="runtime_core_write_blocked",
|
|
542
|
+
severity="error",
|
|
543
|
+
evidence=(
|
|
544
|
+
f"{tool_name} attempted on protected runtime core path {filepath}. "
|
|
545
|
+
"Install-time core files must be changed through the source repo/release flow, "
|
|
546
|
+
"not by editing ~/.nexo/core in place."
|
|
547
|
+
),
|
|
548
|
+
file_token=filepath,
|
|
549
|
+
)
|
|
550
|
+
blocks.append(
|
|
551
|
+
{
|
|
552
|
+
"file": filepath,
|
|
553
|
+
"task_id": "",
|
|
554
|
+
"debt_id": debt.get("id"),
|
|
555
|
+
"debt_type": "runtime_core_write_blocked",
|
|
556
|
+
"reason_code": "runtime_core_protected",
|
|
557
|
+
}
|
|
558
|
+
)
|
|
559
|
+
return blocks
|
|
560
|
+
|
|
561
|
+
|
|
523
562
|
def _read_claude_session_id_from_coordination() -> str:
|
|
524
563
|
"""Fallback claude_session_id when Claude Code's PreToolUse payload omits it.
|
|
525
564
|
|
|
@@ -598,6 +637,23 @@ def process_pre_tool_event(payload: dict) -> dict:
|
|
|
598
637
|
"status": "blocked",
|
|
599
638
|
}
|
|
600
639
|
|
|
640
|
+
core_blocks = _collect_runtime_core_write_blocks(
|
|
641
|
+
conn,
|
|
642
|
+
sid=sid,
|
|
643
|
+
tool_name=tool_name,
|
|
644
|
+
files=files,
|
|
645
|
+
)
|
|
646
|
+
if core_blocks:
|
|
647
|
+
return {
|
|
648
|
+
"ok": True,
|
|
649
|
+
"session_id": sid,
|
|
650
|
+
"tool_name": tool_name,
|
|
651
|
+
"operation": op,
|
|
652
|
+
"strictness": strictness,
|
|
653
|
+
"blocks": core_blocks,
|
|
654
|
+
"status": "blocked",
|
|
655
|
+
}
|
|
656
|
+
|
|
601
657
|
if strictness == "lenient":
|
|
602
658
|
return {"ok": True, "skipped": True, "reason": "lenient mode", "strictness": strictness}
|
|
603
659
|
|
|
@@ -904,6 +960,11 @@ def format_pretool_block_message(result: dict) -> str:
|
|
|
904
960
|
f"- {file_note}: automation sessions cannot write to the live NEXO repo. "
|
|
905
961
|
"Use an isolated checkout/worktree or the public contribution Draft PR flow."
|
|
906
962
|
)
|
|
963
|
+
elif item.get("reason_code") == "runtime_core_protected":
|
|
964
|
+
lines.append(
|
|
965
|
+
f"- {file_note}: `~/.nexo/core` is a protected install surface. "
|
|
966
|
+
"Route the change through the source repo + release/update flow instead of editing the live installed core."
|
|
967
|
+
)
|
|
907
968
|
elif item.get("reason_code") == "guard_unacknowledged":
|
|
908
969
|
lines.append(
|
|
909
970
|
f"- {file_note}: task {item['task_id']} still has blocking guard debt. Acknowledge it with `nexo_task_acknowledge_guard` before retrying."
|
|
@@ -17,7 +17,15 @@ case "$TOOL_NAME" in
|
|
|
17
17
|
esac
|
|
18
18
|
|
|
19
19
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
20
|
-
|
|
20
|
+
OPERATIONS_DIR="$NEXO_HOME/runtime/operations"
|
|
21
|
+
if [ ! -d "$OPERATIONS_DIR" ] && [ -d "$NEXO_HOME/operations" ]; then
|
|
22
|
+
OPERATIONS_DIR="$NEXO_HOME/operations"
|
|
23
|
+
fi
|
|
24
|
+
DATA_DIR="$NEXO_HOME/runtime/data"
|
|
25
|
+
if [ ! -d "$DATA_DIR" ] && [ -d "$NEXO_HOME/data" ]; then
|
|
26
|
+
DATA_DIR="$NEXO_HOME/data"
|
|
27
|
+
fi
|
|
28
|
+
LOG_DIR="$OPERATIONS_DIR/tool-logs"
|
|
21
29
|
mkdir -p "$LOG_DIR"
|
|
22
30
|
|
|
23
31
|
TODAY=$(date +%Y-%m-%d)
|
|
@@ -58,10 +66,10 @@ print(json.dumps(record))
|
|
|
58
66
|
# ── Layer 1: Auto-diary every 10 tool calls (session-scoped) ─────────
|
|
59
67
|
# Extract session_id for per-session counters (prevents cross-terminal contamination)
|
|
60
68
|
SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id','global'))" 2>/dev/null || echo "global")
|
|
61
|
-
COUNTER_DIR="$
|
|
69
|
+
COUNTER_DIR="$OPERATIONS_DIR/counters"
|
|
62
70
|
mkdir -p "$COUNTER_DIR"
|
|
63
71
|
COUNTER_FILE="$COUNTER_DIR/.tool-call-count-${SESSION_ID}"
|
|
64
|
-
NEXO_DB="$
|
|
72
|
+
NEXO_DB="$DATA_DIR/nexo.db"
|
|
65
73
|
|
|
66
74
|
# Increment counter (atomic: read+write in one step)
|
|
67
75
|
COUNT=1
|
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
# Frequency: Monday, Wednesday, Friday (3x/week)
|
|
7
7
|
|
|
8
8
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
OPERATIONS_DIR="$NEXO_HOME/runtime/operations"
|
|
10
|
+
if [ ! -d "$OPERATIONS_DIR" ] && [ -d "$NEXO_HOME/operations" ]; then
|
|
11
|
+
OPERATIONS_DIR="$NEXO_HOME/operations"
|
|
12
|
+
fi
|
|
13
|
+
mkdir -p "$OPERATIONS_DIR"
|
|
14
|
+
BRIEFING_FILE="$OPERATIONS_DIR/.briefing-last-sent"
|
|
15
|
+
FLAG_FILE="$OPERATIONS_DIR/.briefing-pending"
|
|
11
16
|
TODAY=$(date +%Y-%m-%d)
|
|
12
17
|
HOUR=$(date +%H)
|
|
13
18
|
DOW=$(date +%u) # 1=Monday, 7=Sunday
|
|
@@ -18,7 +18,20 @@ import sys
|
|
|
18
18
|
import time
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
try:
|
|
22
|
+
import paths
|
|
23
|
+
except ModuleNotFoundError as exc:
|
|
24
|
+
if getattr(exc, "name", "") != "paths":
|
|
25
|
+
raise
|
|
26
|
+
|
|
27
|
+
class _PathsFallback:
|
|
28
|
+
@staticmethod
|
|
29
|
+
def operations_dir():
|
|
30
|
+
return Path(os.environ.get("NEXO_HOME", Path.home() / ".nexo")) / "operations"
|
|
31
|
+
|
|
32
|
+
paths = _PathsFallback()
|
|
33
|
+
|
|
34
|
+
STATE_FILE = paths.operations_dir() / ".heartbeat-state.json"
|
|
22
35
|
THRESHOLD = 2
|
|
23
36
|
HEARTBEAT_TOOL = "nexo_heartbeat"
|
|
24
37
|
SKIP_TOOLS = {"nexo_startup", "nexo_stop", "nexo_smart_startup"}
|
|
@@ -9,6 +9,8 @@ NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
|
9
9
|
HELPER=""
|
|
10
10
|
if [ -n "${NEXO_CODE:-}" ] && [ -f "${NEXO_CODE%/}/hooks/heartbeat-enforcement.py" ]; then
|
|
11
11
|
HELPER="${NEXO_CODE%/}/hooks/heartbeat-enforcement.py"
|
|
12
|
+
elif [ -f "$NEXO_HOME/core/hooks/heartbeat-enforcement.py" ]; then
|
|
13
|
+
HELPER="$NEXO_HOME/core/hooks/heartbeat-enforcement.py"
|
|
12
14
|
elif [ -f "$NEXO_HOME/hooks/heartbeat-enforcement.py" ]; then
|
|
13
15
|
HELPER="$NEXO_HOME/hooks/heartbeat-enforcement.py"
|
|
14
16
|
fi
|