nexo-brain 7.2.0 → 7.3.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/hooks/hooks.json +12 -0
- package/package.json +2 -1
- package/src/auto_update.py +128 -17
- package/src/client_sync.py +3 -0
- package/src/crons/manifest.json +12 -0
- package/src/hook_guardrails.py +21 -0
- package/src/hooks/manifest.json +1 -0
- package/src/hooks/pre_tool_use.py +161 -0
- package/src/presets/entities_universal.json +74 -0
- package/src/scripts/guardian_metrics_aggregate.py +177 -0
- package/tool-enforcement-map.json +3714 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""guardian_metrics_aggregate — Plan Consolidado 0.25.
|
|
3
|
+
|
|
4
|
+
Reads ``~/.nexo/logs/guardian-telemetry.ndjson`` (produced per-enqueue
|
|
5
|
+
by ``guardian_telemetry.log_event``) and the latest drift baseline under
|
|
6
|
+
``~/.nexo/reports/drift-baseline-*.json``, and writes a rolling aggregate
|
|
7
|
+
of KPIs to ``~/.nexo/logs/guardian-metrics.ndjson``.
|
|
8
|
+
|
|
9
|
+
KPIs (Plan Consolidado 0.25):
|
|
10
|
+
- capture_rate (injected / triggered)
|
|
11
|
+
- core_rule_violations_per_session (R13/R14/R16/R25/R30 injects / #sessions)
|
|
12
|
+
- declared_done_without_evidence_ratio (R16 hard hits / sessions)
|
|
13
|
+
- false_positive_correction_rate (events tagged fp / events total)
|
|
14
|
+
- avg_minutes_between_guard_check_failures
|
|
15
|
+
|
|
16
|
+
Pure reader: never writes inside the telemetry file. ``NEXO_HOME`` is
|
|
17
|
+
honoured so tests isolate via tmp_path.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import statistics
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Iterable
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
CORE_RULE_IDS = {
|
|
31
|
+
"R13_pre_edit_guard",
|
|
32
|
+
"R14_correction_learning",
|
|
33
|
+
"R16_declared_done",
|
|
34
|
+
"R25_nora_maria_read_only",
|
|
35
|
+
"R30_pre_done_evidence_system_prompt",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _nexo_home() -> Path:
|
|
40
|
+
env = os.environ.get("NEXO_HOME")
|
|
41
|
+
if env:
|
|
42
|
+
return Path(env)
|
|
43
|
+
return Path.home() / ".nexo"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _read_ndjson(path: Path) -> Iterable[dict]:
|
|
47
|
+
try:
|
|
48
|
+
with path.open("r", encoding="utf-8") as fh:
|
|
49
|
+
for line in fh:
|
|
50
|
+
line = line.strip()
|
|
51
|
+
if not line:
|
|
52
|
+
continue
|
|
53
|
+
try:
|
|
54
|
+
yield json.loads(line)
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
continue
|
|
57
|
+
except OSError:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_latest_drift_baseline(home: Path) -> dict | None:
|
|
62
|
+
reports_dir = home / "reports"
|
|
63
|
+
if not reports_dir.is_dir():
|
|
64
|
+
return None
|
|
65
|
+
candidates = sorted(reports_dir.glob("drift-baseline-*.json"))
|
|
66
|
+
if not candidates:
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
return json.loads(candidates[-1].read_text(encoding="utf-8"))
|
|
70
|
+
except (OSError, json.JSONDecodeError):
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def aggregate(home: Path | None = None) -> dict:
|
|
75
|
+
home = home if home is not None else _nexo_home()
|
|
76
|
+
telemetry_path = home / "logs" / "guardian-telemetry.ndjson"
|
|
77
|
+
events = list(_read_ndjson(telemetry_path))
|
|
78
|
+
|
|
79
|
+
by_rule: dict[str, dict[str, int]] = {}
|
|
80
|
+
sessions: set[str] = set()
|
|
81
|
+
core_violations = 0
|
|
82
|
+
r16_hard = 0
|
|
83
|
+
fp_count = 0
|
|
84
|
+
guard_check_failure_ts: list[float] = []
|
|
85
|
+
|
|
86
|
+
for ev in events:
|
|
87
|
+
rid = str(ev.get("rule_id") or ev.get("rule") or "").strip() or "unknown"
|
|
88
|
+
event = str(ev.get("event") or "").strip()
|
|
89
|
+
mode = str(ev.get("mode") or "").strip().lower()
|
|
90
|
+
session_id = str(ev.get("session_id") or ev.get("sid") or "").strip()
|
|
91
|
+
if session_id:
|
|
92
|
+
sessions.add(session_id)
|
|
93
|
+
bucket = by_rule.setdefault(rid, {"triggered": 0, "injected": 0, "fp": 0})
|
|
94
|
+
if event in ("trigger", "fire"):
|
|
95
|
+
bucket["triggered"] += 1
|
|
96
|
+
elif event in ("enqueue", "inject"):
|
|
97
|
+
bucket["injected"] += 1
|
|
98
|
+
bucket["triggered"] += 1
|
|
99
|
+
if rid in CORE_RULE_IDS:
|
|
100
|
+
core_violations += 1
|
|
101
|
+
if rid == "R16_declared_done" and mode == "hard":
|
|
102
|
+
r16_hard += 1
|
|
103
|
+
if ev.get("fp") is True:
|
|
104
|
+
bucket["fp"] += 1
|
|
105
|
+
fp_count += 1
|
|
106
|
+
if event == "guard_check_failed":
|
|
107
|
+
try:
|
|
108
|
+
guard_check_failure_ts.append(float(ev.get("ts") or 0))
|
|
109
|
+
except (TypeError, ValueError):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
total_triggered = sum(b["triggered"] for b in by_rule.values()) or 0
|
|
113
|
+
total_injected = sum(b["injected"] for b in by_rule.values()) or 0
|
|
114
|
+
capture_rate = (total_injected / total_triggered) if total_triggered else 0.0
|
|
115
|
+
|
|
116
|
+
guard_check_failure_ts.sort()
|
|
117
|
+
deltas_min = [
|
|
118
|
+
(b - a) / 60.0
|
|
119
|
+
for a, b in zip(guard_check_failure_ts, guard_check_failure_ts[1:])
|
|
120
|
+
if (b - a) > 0
|
|
121
|
+
]
|
|
122
|
+
avg_min_between = statistics.mean(deltas_min) if deltas_min else None
|
|
123
|
+
|
|
124
|
+
n_sessions = len(sessions) or 1
|
|
125
|
+
|
|
126
|
+
baseline = _read_latest_drift_baseline(home)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
130
|
+
"nexo_home": str(home),
|
|
131
|
+
"events_read": len(events),
|
|
132
|
+
"sessions_seen": len(sessions),
|
|
133
|
+
"capture_rate": round(capture_rate, 4),
|
|
134
|
+
"core_rule_violations_per_session": round(core_violations / n_sessions, 4),
|
|
135
|
+
"declared_done_without_evidence_ratio": round(r16_hard / n_sessions, 4),
|
|
136
|
+
"false_positive_correction_rate": round(
|
|
137
|
+
(fp_count / total_triggered) if total_triggered else 0.0, 4
|
|
138
|
+
),
|
|
139
|
+
"avg_minutes_between_guard_check_failures": avg_min_between,
|
|
140
|
+
"per_rule": {
|
|
141
|
+
rid: {
|
|
142
|
+
"triggered": b["triggered"],
|
|
143
|
+
"injected": b["injected"],
|
|
144
|
+
"fp": b["fp"],
|
|
145
|
+
"baseline_hits": (baseline or {}).get("rule_counts", {}).get(rid, 0),
|
|
146
|
+
}
|
|
147
|
+
for rid, b in by_rule.items()
|
|
148
|
+
},
|
|
149
|
+
"drift_baseline_source": (
|
|
150
|
+
str(sorted((home / "reports").glob("drift-baseline-*.json"))[-1])
|
|
151
|
+
if baseline else None
|
|
152
|
+
),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def write_metrics(result: dict, *, home: Path | None = None) -> Path:
|
|
157
|
+
home = home if home is not None else _nexo_home()
|
|
158
|
+
logs_dir = home / "logs"
|
|
159
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
path = logs_dir / "guardian-metrics.ndjson"
|
|
161
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
162
|
+
fh.write(json.dumps(result, ensure_ascii=False) + "\n")
|
|
163
|
+
return path
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main(argv: list[str] | None = None) -> int:
|
|
167
|
+
result = aggregate()
|
|
168
|
+
path = write_metrics(result)
|
|
169
|
+
print(
|
|
170
|
+
f"guardian_metrics_aggregate: {result['events_read']} events across "
|
|
171
|
+
f"{result['sessions_seen']} sessions → appended to {path}"
|
|
172
|
+
)
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__": # pragma: no cover
|
|
177
|
+
sys.exit(main(sys.argv[1:]))
|