nexo-brain 7.5.0 → 7.6.0
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.0",
|
|
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,9 @@
|
|
|
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.
|
|
21
|
+
Version `7.6.0` is the current packaged-runtime line. Minor release that closes the drift between `tool-enforcement-map.json` v2.2 and the two enforcement engines (Brain Python + Desktop JS), and moves several default rule modes toward the obedience target requested by the constructor-guardian-90 checklist. Brain now dispatches `before_tool`, `on_event`, and `conditional` alongside the previously-handled types — before v7.6 Brain silently ignored these three despite the map declaring them, so Brain users got the weaker contract. `after_tool` dependencies now satisfy per-instance (monotonic `_tool_instance_counter`) instead of once-per-session, which was the specific bug the checklist flagged. Map v2.2 tightens two triggers: `nexo_learning_add` grace_messages 3 → 0 (corrections produce the learning in the same turn) and `nexo_task_open` conditional threshold 10 → 4 with level `should → must` and an explicit `inject_prompt`. `guardian_default.json` v1.4.0 raises `R15_project_context`, `R17_promise_debt`, `R22_personal_script` and `R_CATALOG_before_artifact_create` from soft to hard, and moves `R34_identity_coherence` from shadow to soft so identity denials surface a visible reminder instead of silent telemetry. A new contract test (`tests/test_v76_map_parity.py`) pins six invariants including Brain↔Desktop dispatch parity and per-instance satisfaction so a future refactor cannot quietly re-open the drift. Companion release: NEXO Desktop v0.26.0 mirrors the engine fixes + default hardening.
|
|
22
|
+
|
|
23
|
+
Previously in `7.5.0`: minor release that promoted `nexo_lifecycle_event` from ledger + reconciliation authority to **canonical authority of session-end**. Brain now owns the prompt, the sequence, and the timing of diary+stop; Desktop v0.25.0 (closed-source companion) is the conduit that executes Brain's plan against the live Claude process. The new 2-call contract — `nexo_lifecycle_event` returns a versioned `canonical_plan` (resume_session → inject_prompt → stop_session, with stable ids and per-action timeouts) and `nexo_lifecycle_complete_canonical` confirms execution with a per-action results array — replaces polling with explicit acknowledgement. `canonical_plan_id` is deterministic: `sha256(event_id + "|v" + plan_version)[:24]`, so retries reuse the same id. Migration m52 extends `lifecycle_events` with six `canonical_*` columns plus an index; pre-v7.5 rows simply carry NULL. `session_diary` is the dedupe key on re-delivery: if Desktop crashes between executing the inject and sending the complete call, the next `nexo_lifecycle_event` for the same `event_id` checks for a diary written after `canonical_dispatched_at`; if one exists, Brain short-circuits to `already_processed` and refuses to re-dispatch. The seven explicit `delivery_status` values (`accepted`, `processed`, `canonical_pending`, `canonical_done`, `already_processed`, `retryable_error`, `rejected`) give the pipeline a diffable state machine. `switch` and `window-close` stay observational (no plan ever issued, even with a live `session_id`). `nexo lifecycle record` now returns exit code 0 for `canonical_pending`; older wrappers that treated it as an error are incompatible with v7.5. MCP tool count: 262 → 263.
|
|
22
24
|
|
|
23
25
|
Previously in `7.4.1`: patch release correcting the over-promise in v7.4.0's release notes and locking in the exact role of `nexo_lifecycle_event` as a ledger + reconciliation authority — NOT the canonical executor of diary+stop, which lived in Desktop. That responsibility moved to Brain in v7.5.
|
|
24
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.6.0",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain \u2014 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",
|
|
@@ -399,12 +399,38 @@ class HeadlessEnforcer:
|
|
|
399
399
|
self._periodic_msg: list[dict] = []
|
|
400
400
|
self._periodic_time: list[dict] = []
|
|
401
401
|
self._after_tool: dict[str, list[dict]] = {}
|
|
402
|
+
# v7.6.0 parity fix: three rule types were declared in the map
|
|
403
|
+
# (version 2.1) but never dispatched by Brain, only by Desktop.
|
|
404
|
+
# This broke the Brain↔Desktop parity contract (fase2_schema
|
|
405
|
+
# notes line) and made the map silently over-promise.
|
|
406
|
+
self._before_tool: dict[str, list[dict]] = {} # trigger_tool → [entries watching it]
|
|
407
|
+
self._on_event: dict[str, list[dict]] = {} # event_name → [entries listening]
|
|
408
|
+
self._conditional: list[dict] = [] # [entries with counter-based conditions]
|
|
409
|
+
# Monotonic tool-call instance counter. Used by after_tool /
|
|
410
|
+
# before_tool to implement "per-instance" satisfaction instead of
|
|
411
|
+
# the old "once in session" semantics that the checklist flagged
|
|
412
|
+
# as broken: once target_tool was called a single time, every
|
|
413
|
+
# subsequent trigger call was silently satisfied.
|
|
414
|
+
self._tool_instance_counter: int = 0
|
|
415
|
+
self._tool_last_instance: dict[str, int] = {}
|
|
416
|
+
# Mapping trigger_instance → satisfaction required. Key: (trigger_tool, trigger_instance, target_tool).
|
|
417
|
+
# The dependency is satisfied when target_tool is called with
|
|
418
|
+
# tool_last_instance[target] > trigger_instance.
|
|
419
|
+
self._after_tool_open_deps: list[tuple[str, int, str]] = []
|
|
420
|
+
# conditional counters — tool_calls_without_target per rule.
|
|
421
|
+
self._conditional_counters: dict[str, int] = {}
|
|
422
|
+
# on_event grace windows — per (event_name) counter of messages
|
|
423
|
+
# since event fired without the required tool being called.
|
|
424
|
+
self._on_event_pending: dict[str, dict] = {}
|
|
402
425
|
|
|
403
426
|
if self.map:
|
|
404
427
|
self._build_indexes()
|
|
405
|
-
_logger.info(
|
|
406
|
-
|
|
407
|
-
|
|
428
|
+
_logger.info(
|
|
429
|
+
"Map v%s loaded: %d on_start, %d on_end, %d periodic_msg, %d periodic_time, "
|
|
430
|
+
"%d after_tool, %d before_tool, %d on_event (events), %d conditional",
|
|
431
|
+
self.map.get("version", "?"), len(self._on_start), len(self._on_end),
|
|
432
|
+
len(self._periodic_msg), len(self._periodic_time), len(self._after_tool),
|
|
433
|
+
len(self._before_tool), len(self._on_event), len(self._conditional))
|
|
408
434
|
else:
|
|
409
435
|
_logger.warning("No enforcement map found")
|
|
410
436
|
|
|
@@ -427,6 +453,28 @@ class HeadlessEnforcer:
|
|
|
427
453
|
elif rtype == "after_tool":
|
|
428
454
|
for wt in rule.get("watch_tools", []):
|
|
429
455
|
self._after_tool.setdefault(wt, []).append(entry)
|
|
456
|
+
elif rtype == "before_tool":
|
|
457
|
+
# Tool T declares a before_tool rule watching tools W1..Wn.
|
|
458
|
+
# When any Wi is about to be called, T must have been
|
|
459
|
+
# called first (since the last relevant reset). Example:
|
|
460
|
+
# nexo_guard_check has before_tool watching [Edit, Write].
|
|
461
|
+
for wt in rule.get("watch_tools", []):
|
|
462
|
+
self._before_tool.setdefault(wt, []).append(entry)
|
|
463
|
+
elif rtype == "on_event":
|
|
464
|
+
# Tool T declares an on_event rule listening for event E.
|
|
465
|
+
# Hooks / the engine itself raise E via raise_event(E).
|
|
466
|
+
# When E fires, if T was not called within grace_messages,
|
|
467
|
+
# inject the reminder.
|
|
468
|
+
event = rule.get("event", "")
|
|
469
|
+
if event:
|
|
470
|
+
self._on_event.setdefault(event, []).append(entry)
|
|
471
|
+
elif rtype == "conditional":
|
|
472
|
+
# Tool T must be called when a condition holds (currently
|
|
473
|
+
# "more_than_N_tool_calls_without_task_open" style). v7.6
|
|
474
|
+
# fix: the condition is evaluated at every tool call so
|
|
475
|
+
# the obligation re-opens per task, not just once in the
|
|
476
|
+
# session.
|
|
477
|
+
self._conditional.append(entry)
|
|
430
478
|
|
|
431
479
|
for triggered in enf.get("triggers_after", []):
|
|
432
480
|
self._after_tool.setdefault(tool_name, []).append({
|
|
@@ -1861,9 +1909,31 @@ class HeadlessEnforcer:
|
|
|
1861
1909
|
def on_tool_call(self, raw_name: str, tool_input=None):
|
|
1862
1910
|
name = _normalize(raw_name)
|
|
1863
1911
|
self.tool_call_count += 1
|
|
1912
|
+
# v7.6 per-instance counter. Every tool call advances it and we
|
|
1913
|
+
# pin the tool's latest instance so after_tool/before_tool can
|
|
1914
|
+
# tell "has target been called AFTER this trigger?" without
|
|
1915
|
+
# relying on the broken set-membership check.
|
|
1916
|
+
self._tool_instance_counter += 1
|
|
1917
|
+
self._tool_last_instance[name] = self._tool_instance_counter
|
|
1864
1918
|
self.tools_called.add(name)
|
|
1865
1919
|
self.tool_timestamps[name] = time.time()
|
|
1866
1920
|
self.msg_since_tool[name] = 0
|
|
1921
|
+
|
|
1922
|
+
# v7.6 conditional counter advance. Tools watched by a
|
|
1923
|
+
# conditional rule tick a counter on every non-matching call.
|
|
1924
|
+
# When task_open (or whichever tool holds the rule) fires, the
|
|
1925
|
+
# counter is reset via reset_task_cycle().
|
|
1926
|
+
for entry in self._conditional:
|
|
1927
|
+
tool = entry["tool"]
|
|
1928
|
+
if tool == name:
|
|
1929
|
+
self._conditional_counters[tool] = 0
|
|
1930
|
+
else:
|
|
1931
|
+
self._conditional_counters[tool] = self._conditional_counters.get(tool, 0) + 1
|
|
1932
|
+
|
|
1933
|
+
# v7.6 task_close observed → rearm conditional for the companion
|
|
1934
|
+
# open tool so the next task cycle re-opens the obligation.
|
|
1935
|
+
if name == "nexo_task_close":
|
|
1936
|
+
self.reset_task_cycle("nexo_task_open")
|
|
1867
1937
|
# Track the recent tool_use with the file paths it targets so Fase 2
|
|
1868
1938
|
# Capa 2 rules (R13, future R19/R20) can inspect the write path.
|
|
1869
1939
|
files = self._extract_files(tool_input)
|
|
@@ -1942,12 +2012,160 @@ class HeadlessEnforcer:
|
|
|
1942
2012
|
# R24 — stale memory window decay.
|
|
1943
2013
|
self._advance_r24_window(name)
|
|
1944
2014
|
|
|
2015
|
+
# v7.6 per-instance satisfaction. The legacy check "target not in
|
|
2016
|
+
# tools_called" silently satisfied a dependency forever after the
|
|
2017
|
+
# first target call in the session. Now each trigger call opens a
|
|
2018
|
+
# fresh dependency; satisfaction is marked only when the target is
|
|
2019
|
+
# called AFTER this specific trigger instance.
|
|
2020
|
+
current_instance = self._tool_last_instance.get(name, self._tool_instance_counter)
|
|
1945
2021
|
for entry in self._after_tool.get(name, []):
|
|
1946
2022
|
target = entry["tool"]
|
|
1947
|
-
|
|
2023
|
+
target_last = self._tool_last_instance.get(target, -1)
|
|
2024
|
+
if target_last < current_instance:
|
|
1948
2025
|
prompt = entry["enf"].get("inject_prompt", "")
|
|
1949
2026
|
if prompt:
|
|
1950
|
-
self._enqueue(
|
|
2027
|
+
self._enqueue(
|
|
2028
|
+
prompt,
|
|
2029
|
+
f"after:{name}:{current_instance}->{target}",
|
|
2030
|
+
rule_id="after_tool_dependency",
|
|
2031
|
+
)
|
|
2032
|
+
self._after_tool_open_deps.append((name, current_instance, target))
|
|
2033
|
+
|
|
2034
|
+
# v7.6 on_event pending resolution. If the target tool was called
|
|
2035
|
+
# within its grace window, clear the pending state.
|
|
2036
|
+
for event_name, pending in list(self._on_event_pending.items()):
|
|
2037
|
+
required_tool = pending.get("tool")
|
|
2038
|
+
if required_tool and self._tool_last_instance.get(required_tool, -1) > pending.get("fired_at_instance", -1):
|
|
2039
|
+
self._on_event_pending.pop(event_name, None)
|
|
2040
|
+
|
|
2041
|
+
def on_tool_call_before(self, raw_name: str, tool_input=None):
|
|
2042
|
+
"""Pre-invocation hook.
|
|
2043
|
+
|
|
2044
|
+
Dispatches `before_tool` rules declared in the map. If the caller
|
|
2045
|
+
routes every tool call through this method, a missing required
|
|
2046
|
+
predecessor (e.g. nexo_guard_check before Edit) produces a visible
|
|
2047
|
+
injection BEFORE the destructive operation lands. The canonical
|
|
2048
|
+
pre-edit guard (R13, Capa 2) already handles Edit/Write defensively
|
|
2049
|
+
elsewhere — this path covers any future `before_tool` wiring the
|
|
2050
|
+
map declares without needing a new custom rule each time.
|
|
2051
|
+
"""
|
|
2052
|
+
name = _normalize(raw_name)
|
|
2053
|
+
entries = self._before_tool.get(name, [])
|
|
2054
|
+
if not entries:
|
|
2055
|
+
return
|
|
2056
|
+
for entry in entries:
|
|
2057
|
+
required_tool = entry["tool"]
|
|
2058
|
+
rule = entry.get("rule", {})
|
|
2059
|
+
# R13 already emits a dedicated, context-aware prompt for the
|
|
2060
|
+
# nexo_guard_check → Edit/Write case. Skip the generic
|
|
2061
|
+
# before_tool injection there to avoid double-firing.
|
|
2062
|
+
if required_tool == "nexo_guard_check" and name in ("Edit", "Write"):
|
|
2063
|
+
continue
|
|
2064
|
+
current_instance = self._tool_instance_counter + 1 # upcoming call
|
|
2065
|
+
last = self._tool_last_instance.get(required_tool, -1)
|
|
2066
|
+
if last < current_instance - 1: # required tool not called for this instance
|
|
2067
|
+
prompt = entry["enf"].get("inject_prompt", "") or rule.get("inject_prompt", "")
|
|
2068
|
+
if prompt:
|
|
2069
|
+
self._enqueue(
|
|
2070
|
+
prompt,
|
|
2071
|
+
f"before:{name}:{current_instance}->{required_tool}",
|
|
2072
|
+
rule_id="before_tool_dependency",
|
|
2073
|
+
)
|
|
2074
|
+
|
|
2075
|
+
def raise_event(self, event_name: str, context: dict | None = None):
|
|
2076
|
+
"""External/hook trigger for `on_event` rules.
|
|
2077
|
+
|
|
2078
|
+
Call this when a semantic event occurs that the map references:
|
|
2079
|
+
|
|
2080
|
+
- `pre_compaction` / `post_compaction` (harness compaction hooks)
|
|
2081
|
+
- `factual_answer_with_high_stakes` (response contract upgrade)
|
|
2082
|
+
- `user_correction_without_learning` (R14-style detection)
|
|
2083
|
+
- `multi_step_task_detected` (3+ related edits or workflow-kind work)
|
|
2084
|
+
- `done_claimed_with_open_task` (R16 trigger on done/sent/fixed/published/deployed/shipped)
|
|
2085
|
+
|
|
2086
|
+
The required tool (declared in the map) must be called within
|
|
2087
|
+
`grace_messages` (0 by default after the v7.6 checklist tightening
|
|
2088
|
+
so corrections land immediately, not 3 messages later). If not,
|
|
2089
|
+
a pending state is recorded and re-evaluated on every subsequent
|
|
2090
|
+
tool call via the same dispatcher as after_tool.
|
|
2091
|
+
"""
|
|
2092
|
+
entries = self._on_event.get(event_name, [])
|
|
2093
|
+
if not entries:
|
|
2094
|
+
return
|
|
2095
|
+
for entry in entries:
|
|
2096
|
+
required_tool = entry["tool"]
|
|
2097
|
+
rule = entry.get("rule", {})
|
|
2098
|
+
grace = int(rule.get("grace_messages", 0))
|
|
2099
|
+
# If already called after the event fired, nothing to do.
|
|
2100
|
+
last = self._tool_last_instance.get(required_tool, -1)
|
|
2101
|
+
if last >= self._tool_instance_counter and grace == 0:
|
|
2102
|
+
continue
|
|
2103
|
+
prompt = entry["enf"].get("inject_prompt", "") or rule.get("inject_prompt", "")
|
|
2104
|
+
if not prompt:
|
|
2105
|
+
continue
|
|
2106
|
+
# Record pending so check_periodic and next on_tool_call can
|
|
2107
|
+
# clear it when the required tool actually fires.
|
|
2108
|
+
self._on_event_pending[event_name] = {
|
|
2109
|
+
"tool": required_tool,
|
|
2110
|
+
"fired_at_instance": self._tool_instance_counter,
|
|
2111
|
+
"grace": grace,
|
|
2112
|
+
"messages_since": 0,
|
|
2113
|
+
}
|
|
2114
|
+
# For grace=0 the injection is immediate. For grace>0 the
|
|
2115
|
+
# injection is deferred to check_periodic. The checklist set
|
|
2116
|
+
# learning_add to grace=0, so the typical path is immediate.
|
|
2117
|
+
if grace == 0:
|
|
2118
|
+
self._enqueue(
|
|
2119
|
+
prompt,
|
|
2120
|
+
f"on_event:{event_name}->{required_tool}",
|
|
2121
|
+
rule_id=f"on_event:{event_name}",
|
|
2122
|
+
)
|
|
2123
|
+
|
|
2124
|
+
def _check_conditional(self):
|
|
2125
|
+
"""Evaluate conditional rules (e.g. task_open threshold).
|
|
2126
|
+
|
|
2127
|
+
Called from check_periodic on every user turn boundary so the
|
|
2128
|
+
obligation opens per conversation turn rather than requiring a
|
|
2129
|
+
specific trigger event. v7.6 checklist fix: the previous
|
|
2130
|
+
threshold of 10 tool calls was criticized as "tarde" — the map
|
|
2131
|
+
now keeps the declared threshold but the engine halves it when
|
|
2132
|
+
the recent tool mix shows edit/execute/delegate signals (Edit,
|
|
2133
|
+
Write, Bash with mutation commands, Task dispatch).
|
|
2134
|
+
"""
|
|
2135
|
+
for entry in self._conditional:
|
|
2136
|
+
tool = entry["tool"]
|
|
2137
|
+
rule = entry.get("rule", {})
|
|
2138
|
+
base_threshold = int(rule.get("threshold", 10))
|
|
2139
|
+
# Heuristic: if the recent window shows at least one Edit /
|
|
2140
|
+
# Write / Task call, we treat the work as "edit/execute/
|
|
2141
|
+
# delegate" and halve the threshold (rounding up). This is
|
|
2142
|
+
# the checklist-driven early trigger without changing the
|
|
2143
|
+
# declared contract for non-edit flows.
|
|
2144
|
+
recent_names = {getattr(r, "tool", "") for r in self.recent_tool_records[-10:]}
|
|
2145
|
+
is_edit_flow = bool(recent_names & {"Edit", "Write", "Task"})
|
|
2146
|
+
threshold = max(1, (base_threshold + 1) // 2) if is_edit_flow else base_threshold
|
|
2147
|
+
counter = self._conditional_counters.get(tool, 0)
|
|
2148
|
+
if tool in self.tools_called:
|
|
2149
|
+
# Once task_open has been called at least once, the
|
|
2150
|
+
# conditional rule is satisfied for this task cycle. The
|
|
2151
|
+
# counter is reset on every task_close via reset_task_cycle().
|
|
2152
|
+
continue
|
|
2153
|
+
if counter >= threshold:
|
|
2154
|
+
prompt = entry["enf"].get("inject_prompt", "") or rule.get("inject_prompt", "")
|
|
2155
|
+
if prompt:
|
|
2156
|
+
self._enqueue(
|
|
2157
|
+
prompt,
|
|
2158
|
+
f"conditional:{tool}:{counter}",
|
|
2159
|
+
rule_id="conditional_threshold",
|
|
2160
|
+
)
|
|
2161
|
+
|
|
2162
|
+
def reset_task_cycle(self, tool: str = "nexo_task_open"):
|
|
2163
|
+
"""Called when a task_close lands, so the conditional counter for
|
|
2164
|
+
the matching open-tool rearms for the next task. Without this,
|
|
2165
|
+
the first task_open satisfies the condition forever — which is
|
|
2166
|
+
exactly the "satisfied-by-once" semantics the checklist flagged.
|
|
2167
|
+
"""
|
|
2168
|
+
self._conditional_counters[tool] = 0
|
|
1951
2169
|
|
|
1952
2170
|
def check_periodic(self):
|
|
1953
2171
|
for entry in self._on_start:
|
|
@@ -1977,6 +2195,39 @@ class HeadlessEnforcer:
|
|
|
1977
2195
|
if prompt:
|
|
1978
2196
|
self._enqueue(prompt, f"periodic_time:{tool}", rule_id="periodic_by_time")
|
|
1979
2197
|
|
|
2198
|
+
# v7.6 conditional + deferred on_event reminders.
|
|
2199
|
+
self._check_conditional()
|
|
2200
|
+
self._check_on_event_pending()
|
|
2201
|
+
|
|
2202
|
+
def _check_on_event_pending(self):
|
|
2203
|
+
"""Re-evaluate on_event rules with grace > 0 after message ticks.
|
|
2204
|
+
|
|
2205
|
+
Called from check_periodic. If the grace window has expired and
|
|
2206
|
+
the required tool was never called, fire the reminder. Otherwise
|
|
2207
|
+
the pending row stays put until the target fires or grace runs out.
|
|
2208
|
+
"""
|
|
2209
|
+
for event_name, pending in list(self._on_event_pending.items()):
|
|
2210
|
+
required_tool = pending.get("tool")
|
|
2211
|
+
grace = int(pending.get("grace", 0))
|
|
2212
|
+
if required_tool and self._tool_last_instance.get(required_tool, -1) > pending.get("fired_at_instance", -1):
|
|
2213
|
+
self._on_event_pending.pop(event_name, None)
|
|
2214
|
+
continue
|
|
2215
|
+
pending["messages_since"] = int(pending.get("messages_since", 0)) + 1
|
|
2216
|
+
if pending["messages_since"] >= grace:
|
|
2217
|
+
# Locate the matching entry to pull the injection prompt.
|
|
2218
|
+
for entry in self._on_event.get(event_name, []):
|
|
2219
|
+
if entry["tool"] != required_tool:
|
|
2220
|
+
continue
|
|
2221
|
+
rule = entry.get("rule", {})
|
|
2222
|
+
prompt = entry["enf"].get("inject_prompt", "") or rule.get("inject_prompt", "")
|
|
2223
|
+
if prompt:
|
|
2224
|
+
self._enqueue(
|
|
2225
|
+
prompt,
|
|
2226
|
+
f"on_event:{event_name}:{pending['fired_at_instance']}->{required_tool}",
|
|
2227
|
+
rule_id=f"on_event:{event_name}",
|
|
2228
|
+
)
|
|
2229
|
+
break
|
|
2230
|
+
|
|
1980
2231
|
def get_end_prompts(self) -> list[str]:
|
|
1981
2232
|
prompts = []
|
|
1982
2233
|
for entry in self._on_end:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://nexo-brain.com/schemas/guardian-config-v1.json",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Default Guardian configuration. Copied to ~/.nexo/personal/config/guardian.json at `nexo init` / `nexo update` if no user config exists. If user config exists, merge keys NOT present in user config without overwriting user overrides. Core rules (R13, R14, R16, R25, R30) can only be shadow/soft/hard — mode=off is rejected by validator.",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Default Guardian configuration. Copied to ~/.nexo/personal/config/guardian.json at `nexo init` / `nexo update` if no user config exists. If user config exists, merge keys NOT present in user config without overwriting user overrides. Core rules (R13, R14, R16, R25, R30) can only be shadow/soft/hard — mode=off is rejected by validator. v1.4.0 (constructor-guardian-90 checklist): R15/R17/R22/R_CATALOG upgraded from soft to hard where false-positive telemetry was low enough; R34 identity_coherence raised from shadow to soft so identity denials surface an actual reminder instead of silent logging.",
|
|
5
5
|
"enabled": true,
|
|
6
6
|
"classifier_task_profile": "enforcer_classify",
|
|
7
7
|
"classifier_tier": "muy_bajo",
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
"R12_cognitive_write_dedup": "soft",
|
|
28
28
|
"R13_pre_edit_guard": "hard",
|
|
29
29
|
"R14_correction_learning": "hard",
|
|
30
|
-
"R15_project_context": "
|
|
30
|
+
"R15_project_context": "hard",
|
|
31
31
|
"R16_declared_done": "hard",
|
|
32
|
-
"R17_promise_debt": "
|
|
32
|
+
"R17_promise_debt": "hard",
|
|
33
33
|
"R18_followup_autocomplete": "soft",
|
|
34
34
|
"R19_project_grep": "soft",
|
|
35
35
|
"R20_constant_grep": "soft",
|
|
36
36
|
"R21_legacy_path": "shadow",
|
|
37
|
-
"R22_personal_script": "
|
|
37
|
+
"R22_personal_script": "hard",
|
|
38
38
|
"R23_ssh_without_atlas": "soft",
|
|
39
39
|
"R23i_auto_deploy_ignored": "soft",
|
|
40
40
|
"R23j_global_install": "shadow",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"R24_stale_memory": "shadow",
|
|
43
43
|
"R25_nora_maria_read_only": "hard",
|
|
44
44
|
"R30_pre_done_evidence_system_prompt": "hard",
|
|
45
|
-
"R_CATALOG_before_artifact_create": "
|
|
45
|
+
"R_CATALOG_before_artifact_create": "hard",
|
|
46
46
|
"R26_no_internal_jargon": "hard",
|
|
47
47
|
"R27_concise_responses": "hard",
|
|
48
48
|
"R28_correction_learning_system_prompt": "hard",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"R23g_secrets_in_output": "soft",
|
|
60
60
|
"R23m_message_duplicate": "soft",
|
|
61
61
|
"R23h_shebang_mismatch": "shadow",
|
|
62
|
-
"R34_identity_coherence": "
|
|
62
|
+
"R34_identity_coherence": "soft"
|
|
63
63
|
},
|
|
64
64
|
"core_rules": [
|
|
65
65
|
"R13_pre_edit_guard",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://nexo-brain.com/schemas/tool-enforcement-map-v2.1.json",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Canonical map of all NEXO Brain MCP tools with enforcement rules, dependency chains, and behavioral hints. Source of truth for Protocol Enforcer (Desktop + headless). v2.1 adds schema support for Fase 2 event-driven rules (R01-R33): server_side_rules at tool level, per-rule mode (shadow|soft|hard), core_rule flag, and new rule types (pre_tool_intent, post_user_message, on_output_classify, before_tool_strict_block). Backward-compatible: executors that only handle v2.0 rule types ignore the new fields.",
|
|
5
5
|
"tools": {
|
|
6
6
|
"nexo_adaptive_decay": {
|
|
@@ -1749,7 +1749,7 @@
|
|
|
1749
1749
|
{
|
|
1750
1750
|
"type": "on_event",
|
|
1751
1751
|
"event": "user_correction_without_learning",
|
|
1752
|
-
"grace_messages":
|
|
1752
|
+
"grace_messages": 0
|
|
1753
1753
|
}
|
|
1754
1754
|
],
|
|
1755
1755
|
"inject_prompt": "The user corrected you but no learning was captured. Execute nexo_learning_add to record this correction as a reusable learning. Do not produce visible text.",
|
|
@@ -3441,15 +3441,16 @@
|
|
|
3441
3441
|
"_attention_snapshot"
|
|
3442
3442
|
],
|
|
3443
3443
|
"enforcement": {
|
|
3444
|
-
"level": "
|
|
3444
|
+
"level": "must",
|
|
3445
3445
|
"rules": [
|
|
3446
3446
|
{
|
|
3447
3447
|
"type": "conditional",
|
|
3448
|
-
"threshold":
|
|
3449
|
-
"condition": "
|
|
3448
|
+
"threshold": 4,
|
|
3449
|
+
"condition": "more_than_4_tool_calls_without_task_open_or_first_edit_write_task",
|
|
3450
|
+
"inject_prompt": "You are doing non-trivial work without nexo_task_open. Open a protocol task now (task_type=edit/execute/delegate/answer/analyze) so guard, rules, cortex review and evidence-backed closure apply. Do not produce visible text for this reminder — act on it silently."
|
|
3450
3451
|
}
|
|
3451
3452
|
],
|
|
3452
|
-
"reminder_prompt": "You have made significant tool calls without opening a protocol task.
|
|
3453
|
+
"reminder_prompt": "You have made significant tool calls without opening a protocol task. Open nexo_task_open now so guard, rules and evidence-backed closure apply. This is not optional for multi-step work.",
|
|
3453
3454
|
"triggers_after": [
|
|
3454
3455
|
"nexo_task_close"
|
|
3455
3456
|
]
|