nexo-brain 7.23.11 → 7.23.13
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 +5 -3
- package/package.json +1 -1
- package/src/db_guard.py +43 -8
- package/src/doctor/providers/boot.py +11 -6
- package/src/guardrails/__init__.py +0 -0
- package/src/guardrails/minimal_delta.py +267 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.23.
|
|
3
|
+
"version": "7.23.13",
|
|
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,11 +18,13 @@
|
|
|
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.23.
|
|
21
|
+
Version `7.23.13` is the current packaged-runtime line. Patch over v7.23.12 - release guardrails now audit publish workflows for masked failures and add minimal-delta coverage for punctual UI edits.
|
|
22
22
|
|
|
23
|
-
Previously in `7.23.
|
|
23
|
+
Previously in `7.23.12`: patch over v7.23.11 - protected database recovery now repairs degraded Brain tables from backup without rolling back newer rows.
|
|
24
|
+
|
|
25
|
+
Previously in `7.23.11`: patch over v7.23.10 - older installed runtimes can update safely even when `cognitive_paths.py` has not been synced yet.
|
|
24
26
|
|
|
25
|
-
Previously in `7.23.
|
|
27
|
+
Previously in `7.23.6`: patch over v7.23.5 - `nexo update` clears safe legacy `cognitive.db` shadows and keeps superseded archives under runtime backup retention.
|
|
26
28
|
|
|
27
29
|
Previously in `7.23.3`: patch over v7.23.2 - Followup runner skips DONE terminal statuses so already-finished followups do not re-enter executable batches.
|
|
28
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.23.
|
|
3
|
+
"version": "7.23.13",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
package/src/db_guard.py
CHANGED
|
@@ -29,7 +29,7 @@ auto_update.py):
|
|
|
29
29
|
diff_row_counts(current, reference, tables) -> WipeReport
|
|
30
30
|
safe_sqlite_backup(source, dest) -> tuple[bool, str | None]
|
|
31
31
|
validate_backup_matches_source(source, dest, tables) -> tuple[bool, str | None]
|
|
32
|
-
restore_tables_from_backup(source, target, tables) -> dict
|
|
32
|
+
restore_tables_from_backup(source, target, tables, mode) -> dict
|
|
33
33
|
kill_nexo_mcp_servers(dry_run) -> dict
|
|
34
34
|
quiesce_nexo_db_writers(dry_run) -> dict
|
|
35
35
|
resume_nexo_launchagents(labels, dry_run) -> dict
|
|
@@ -501,23 +501,42 @@ def _quote_identifier(identifier: str) -> str:
|
|
|
501
501
|
return '"' + identifier.replace('"', '""') + '"'
|
|
502
502
|
|
|
503
503
|
|
|
504
|
+
def _quote_sql_name(identifier: str) -> str:
|
|
505
|
+
return '"' + identifier.replace('"', '""') + '"'
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _table_columns(conn: sqlite3.Connection, schema: str, table: str) -> list[str]:
|
|
509
|
+
if schema not in {"main", "backup_db"}:
|
|
510
|
+
raise ValueError(f"refusing unsafe schema identifier: {schema!r}")
|
|
511
|
+
quoted = _quote_identifier(table)
|
|
512
|
+
rows = conn.execute(f"PRAGMA {schema}.table_info({quoted})").fetchall()
|
|
513
|
+
return [str(row[1]) for row in rows]
|
|
514
|
+
|
|
515
|
+
|
|
504
516
|
def restore_tables_from_backup(
|
|
505
517
|
source: str | Path,
|
|
506
518
|
target: str | Path,
|
|
507
519
|
tables: tuple[str, ...] = LOCAL_CONTEXT_TABLES,
|
|
520
|
+
*,
|
|
521
|
+
mode: str = "replace",
|
|
508
522
|
) -> dict:
|
|
509
|
-
"""
|
|
523
|
+
"""Restore selected tables in ``target`` from ``source``.
|
|
510
524
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
525
|
+
``mode="replace"`` keeps the historical behavior: target rows are deleted
|
|
526
|
+
and replaced by the backup table. ``mode="merge_missing"`` preserves target
|
|
527
|
+
rows and inserts missing rows from the backup with ``INSERT OR IGNORE``.
|
|
528
|
+
This is intentionally table-scoped so repair can recover data without
|
|
529
|
+
rolling back unrelated Brain state created after the backup.
|
|
514
530
|
"""
|
|
531
|
+
if mode not in {"replace", "merge_missing"}:
|
|
532
|
+
raise ValueError(f"unsupported restore mode: {mode!r}")
|
|
515
533
|
src = Path(source)
|
|
516
534
|
dst = Path(target)
|
|
517
535
|
result: dict = {
|
|
518
536
|
"ok": False,
|
|
519
537
|
"source": str(src),
|
|
520
538
|
"target": str(dst),
|
|
539
|
+
"mode": mode,
|
|
521
540
|
"tables": {},
|
|
522
541
|
"errors": [],
|
|
523
542
|
}
|
|
@@ -553,13 +572,29 @@ def restore_tables_from_backup(
|
|
|
553
572
|
continue
|
|
554
573
|
conn.execute(create_sql)
|
|
555
574
|
before = _table_count(conn, table) or 0
|
|
556
|
-
|
|
557
|
-
|
|
575
|
+
if mode == "replace":
|
|
576
|
+
conn.execute(f"DELETE FROM main.{quoted}")
|
|
577
|
+
conn.execute(f"INSERT INTO main.{quoted} SELECT * FROM backup_db.{quoted}")
|
|
578
|
+
status = "restored"
|
|
579
|
+
else:
|
|
580
|
+
target_columns = _table_columns(conn, "main", table)
|
|
581
|
+
source_columns = set(_table_columns(conn, "backup_db", table))
|
|
582
|
+
common_columns = [column for column in target_columns if column in source_columns]
|
|
583
|
+
if not common_columns:
|
|
584
|
+
result["tables"][table] = {"status": "no_common_columns", "before": int(before)}
|
|
585
|
+
continue
|
|
586
|
+
column_sql = ", ".join(_quote_sql_name(column) for column in common_columns)
|
|
587
|
+
conn.execute(
|
|
588
|
+
f"INSERT OR IGNORE INTO main.{quoted} ({column_sql}) "
|
|
589
|
+
f"SELECT {column_sql} FROM backup_db.{quoted}"
|
|
590
|
+
)
|
|
591
|
+
status = "merged"
|
|
558
592
|
after = _table_count(conn, table) or 0
|
|
559
593
|
result["tables"][table] = {
|
|
560
|
-
"status":
|
|
594
|
+
"status": status,
|
|
561
595
|
"before": int(before),
|
|
562
596
|
"after": int(after),
|
|
597
|
+
"restored": max(int(after) - int(before), 0),
|
|
563
598
|
}
|
|
564
599
|
conn.commit()
|
|
565
600
|
result["ok"] = not result["errors"]
|
|
@@ -202,21 +202,26 @@ def check_db_integrity(fix: bool = False) -> DoctorCheck:
|
|
|
202
202
|
evidence.extend(["Local memory regression:", *local_regression.summary_lines()])
|
|
203
203
|
|
|
204
204
|
if fix and recoverable_regression:
|
|
205
|
-
report = restore_tables_from_backup(
|
|
205
|
+
report = restore_tables_from_backup(
|
|
206
|
+
reference,
|
|
207
|
+
db_path,
|
|
208
|
+
tables=PROTECTED_TABLES,
|
|
209
|
+
mode="merge_missing",
|
|
210
|
+
)
|
|
206
211
|
if report.get("ok"):
|
|
207
212
|
restored = {
|
|
208
213
|
table: payload
|
|
209
214
|
for table, payload in (report.get("tables") or {}).items()
|
|
210
|
-
if isinstance(payload, dict) and payload.get("status")
|
|
215
|
+
if isinstance(payload, dict) and payload.get("status") in {"restored", "merged"}
|
|
211
216
|
}
|
|
212
|
-
restored_rows = sum(int(payload.get("
|
|
217
|
+
restored_rows = sum(int(payload.get("restored") or 0) for payload in restored.values())
|
|
213
218
|
return DoctorCheck(
|
|
214
219
|
id="boot.db_integrity",
|
|
215
220
|
tier="boot",
|
|
216
221
|
status="healthy",
|
|
217
222
|
severity="info",
|
|
218
|
-
summary=f"
|
|
219
|
-
evidence=evidence + [f"Restored
|
|
223
|
+
summary=f"Database protected tables restored from backup ({restored_rows} rows recovered)",
|
|
224
|
+
evidence=evidence + [f"Restored protected tables: {len(restored)}"],
|
|
220
225
|
fixed=True,
|
|
221
226
|
)
|
|
222
227
|
return DoctorCheck(
|
|
@@ -224,7 +229,7 @@ def check_db_integrity(fix: bool = False) -> DoctorCheck:
|
|
|
224
229
|
tier="boot",
|
|
225
230
|
status="critical",
|
|
226
231
|
severity="error",
|
|
227
|
-
summary="
|
|
232
|
+
summary="Database protected-table repair failed",
|
|
228
233
|
evidence=evidence + [f"Restore errors: {report.get('errors') or []}"],
|
|
229
234
|
repair_plan=["Close NEXO Desktop and run nexo doctor --tier boot --plane database_real --fix"],
|
|
230
235
|
)
|
|
File without changes
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Minimal-delta pre-mutation guardrail (NF-DS-C3E64B2B).
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
-------
|
|
5
|
+
Block scope creep when the operator describes a *punctual* UI change
|
|
6
|
+
("add this text", "change the size", "adjust the color") but the agent
|
|
7
|
+
proposes a diff that touches many unrelated lines.
|
|
8
|
+
|
|
9
|
+
How it plugs in
|
|
10
|
+
---------------
|
|
11
|
+
Wire ``check`` from ``hook_guardrails.process_pre_tool_event`` BEFORE the
|
|
12
|
+
``Edit`` / ``Write`` call reaches the model. The hook should:
|
|
13
|
+
|
|
14
|
+
1. Call ``classify_request(prompt_text)`` to detect punctual UI verbs.
|
|
15
|
+
2. If punctual, capture the *target file* + *proposed new_string* from the
|
|
16
|
+
pending tool payload.
|
|
17
|
+
3. Read ``read_file_history(path)`` to keep the prior context available
|
|
18
|
+
(it returns the last 5 commits + current text so the agent can reason
|
|
19
|
+
in bullet-by-bullet form when replying).
|
|
20
|
+
4. Call ``check_diff(prompt_text, old_text, new_text)`` and act on the
|
|
21
|
+
returned ``GuardDecision``:
|
|
22
|
+
|
|
23
|
+
* ``decision == "allow"`` → let the tool through, no annotation.
|
|
24
|
+
* ``decision == "warn"`` → let it through but tell the agent in the
|
|
25
|
+
response payload how many extra lines it touched.
|
|
26
|
+
* ``decision == "block"`` → deny the tool with the included reason.
|
|
27
|
+
The agent must reply with a bullet-by-bullet diff against the prior
|
|
28
|
+
state and request explicit confirmation before retrying.
|
|
29
|
+
|
|
30
|
+
Why headless-friendly
|
|
31
|
+
---------------------
|
|
32
|
+
This module is *pure*. No filesystem, no subprocess, no MCP call. The hook
|
|
33
|
+
layer wires it in. That keeps the gate testable in isolation and means a
|
|
34
|
+
wrong tuning here cannot brick the pre-tool pipeline.
|
|
35
|
+
|
|
36
|
+
Origin: Deep Sleep followup NF-DS-C3E64B2B (scope creep in punctual visual
|
|
37
|
+
changes).
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import difflib
|
|
42
|
+
import re
|
|
43
|
+
import subprocess
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from typing import Iterable
|
|
47
|
+
|
|
48
|
+
# Verbs that signal a *punctual* UI change. Matched case-insensitive against
|
|
49
|
+
# the full request text. Spanish terms are intentionally included because the
|
|
50
|
+
# operator often reports UI issues in Spanish; output remains English.
|
|
51
|
+
PUNCTUAL_VERBS: tuple[str, ...] = (
|
|
52
|
+
"añade",
|
|
53
|
+
"anade",
|
|
54
|
+
"agrega",
|
|
55
|
+
"añadir",
|
|
56
|
+
"agrega el texto",
|
|
57
|
+
"cambia el texto",
|
|
58
|
+
"cambia el color",
|
|
59
|
+
"cambia el tamaño",
|
|
60
|
+
"cambia el tamano",
|
|
61
|
+
"ajusta el tamaño",
|
|
62
|
+
"ajusta el tamano",
|
|
63
|
+
"ajusta el padding",
|
|
64
|
+
"ajusta el margen",
|
|
65
|
+
"ajusta el color",
|
|
66
|
+
"renombra",
|
|
67
|
+
"rename",
|
|
68
|
+
"tweak",
|
|
69
|
+
"change the text",
|
|
70
|
+
"change the color",
|
|
71
|
+
"adjust the size",
|
|
72
|
+
"fix the label",
|
|
73
|
+
"fix the wording",
|
|
74
|
+
"add the text",
|
|
75
|
+
"swap the icon",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# File extensions considered UI surfaces. Anything else short-circuits to
|
|
79
|
+
# "not a UI mutation" so we never block server code on accident.
|
|
80
|
+
UI_EXTENSIONS: frozenset[str] = frozenset({
|
|
81
|
+
".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".astro",
|
|
82
|
+
".html", ".css", ".scss", ".sass", ".less",
|
|
83
|
+
".liquid", ".njk", ".hbs",
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
# Default threshold for "unrelated lines". Tuned conservatively — a real UI
|
|
87
|
+
# tweak usually changes 1-3 contiguous lines. Anything > THRESHOLD blocks.
|
|
88
|
+
DEFAULT_THRESHOLD = 8
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class GuardDecision:
|
|
93
|
+
decision: str # "allow" | "warn" | "block"
|
|
94
|
+
matched_verb: str | None
|
|
95
|
+
changed_lines: int
|
|
96
|
+
threshold: int
|
|
97
|
+
reason: str
|
|
98
|
+
|
|
99
|
+
def to_payload(self) -> dict:
|
|
100
|
+
return {
|
|
101
|
+
"guard": "minimal-delta",
|
|
102
|
+
"decision": self.decision,
|
|
103
|
+
"matched_verb": self.matched_verb,
|
|
104
|
+
"changed_lines": self.changed_lines,
|
|
105
|
+
"threshold": self.threshold,
|
|
106
|
+
"reason": self.reason,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def classify_request(prompt_text: str) -> str | None:
|
|
111
|
+
"""Return the matched punctual verb (lowercase) or ``None``."""
|
|
112
|
+
if not prompt_text:
|
|
113
|
+
return None
|
|
114
|
+
haystack = prompt_text.lower()
|
|
115
|
+
for verb in PUNCTUAL_VERBS:
|
|
116
|
+
if verb in haystack:
|
|
117
|
+
return verb
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def is_ui_path(path: str | Path) -> bool:
|
|
122
|
+
if not path:
|
|
123
|
+
return False
|
|
124
|
+
suffix = Path(str(path)).suffix.lower()
|
|
125
|
+
return suffix in UI_EXTENSIONS
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def count_changed_lines(old_text: str, new_text: str) -> int:
|
|
129
|
+
"""Count *non-context* diff lines (+/− only)."""
|
|
130
|
+
if old_text == new_text:
|
|
131
|
+
return 0
|
|
132
|
+
old_lines = old_text.splitlines()
|
|
133
|
+
new_lines = new_text.splitlines()
|
|
134
|
+
diff = difflib.unified_diff(old_lines, new_lines, n=0, lineterm="")
|
|
135
|
+
count = 0
|
|
136
|
+
for line in diff:
|
|
137
|
+
if line.startswith(("+++", "---", "@@")):
|
|
138
|
+
continue
|
|
139
|
+
if line.startswith(("+", "-")):
|
|
140
|
+
count += 1
|
|
141
|
+
return count
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def check_diff(
|
|
145
|
+
prompt_text: str,
|
|
146
|
+
old_text: str,
|
|
147
|
+
new_text: str,
|
|
148
|
+
target_path: str | Path = "",
|
|
149
|
+
*,
|
|
150
|
+
threshold: int = DEFAULT_THRESHOLD,
|
|
151
|
+
) -> GuardDecision:
|
|
152
|
+
verb = classify_request(prompt_text)
|
|
153
|
+
if verb is None:
|
|
154
|
+
return GuardDecision("allow", None, 0, threshold, "Not a punctual request — guard not applied.")
|
|
155
|
+
if target_path and not is_ui_path(target_path):
|
|
156
|
+
return GuardDecision(
|
|
157
|
+
"allow", verb, 0, threshold,
|
|
158
|
+
f"Target {target_path} is not a UI surface — guard not applied.",
|
|
159
|
+
)
|
|
160
|
+
changed = count_changed_lines(old_text, new_text)
|
|
161
|
+
if changed <= 2:
|
|
162
|
+
return GuardDecision(
|
|
163
|
+
"allow", verb, changed, threshold,
|
|
164
|
+
"Diff stays within 2 lines — within the punctual envelope.",
|
|
165
|
+
)
|
|
166
|
+
if changed <= threshold:
|
|
167
|
+
return GuardDecision(
|
|
168
|
+
"warn", verb, changed, threshold,
|
|
169
|
+
(
|
|
170
|
+
f"Punctual request ('{verb}') is changing {changed} lines. "
|
|
171
|
+
"Inside the soft envelope but please justify each line in the reply."
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
return GuardDecision(
|
|
175
|
+
"block", verb, changed, threshold,
|
|
176
|
+
(
|
|
177
|
+
f"Punctual request ('{verb}') is changing {changed} lines (>{threshold}). "
|
|
178
|
+
"Read git log/blame for the file, present the prior state, list each proposed "
|
|
179
|
+
"change as a bullet, and ask the operator before applying. Override only with "
|
|
180
|
+
"explicit confirmation."
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def read_file_history(path: str | Path, *, max_commits: int = 5) -> dict:
|
|
186
|
+
"""Return ``{"current": str, "log": [str], "blame": [str] | None}``.
|
|
187
|
+
|
|
188
|
+
Best-effort: missing git, missing file, or non-zero exit codes are
|
|
189
|
+
swallowed so the guardrail itself never errors out. The dictionary is
|
|
190
|
+
purely informational — the agent must surface it in its reply when the
|
|
191
|
+
guard fires.
|
|
192
|
+
"""
|
|
193
|
+
path_obj = Path(path)
|
|
194
|
+
try:
|
|
195
|
+
current = path_obj.read_text(encoding="utf-8", errors="replace")
|
|
196
|
+
except OSError:
|
|
197
|
+
current = ""
|
|
198
|
+
|
|
199
|
+
log_lines: list[str] = []
|
|
200
|
+
try:
|
|
201
|
+
completed = subprocess.run(
|
|
202
|
+
["git", "log", f"-{max_commits}", "--pretty=format:%h %ad %s", "--date=short", "--", str(path_obj)],
|
|
203
|
+
cwd=path_obj.parent if path_obj.parent.exists() else Path.cwd(),
|
|
204
|
+
capture_output=True,
|
|
205
|
+
text=True,
|
|
206
|
+
timeout=4,
|
|
207
|
+
check=False,
|
|
208
|
+
)
|
|
209
|
+
if completed.returncode == 0 and completed.stdout.strip():
|
|
210
|
+
log_lines = completed.stdout.splitlines()
|
|
211
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
212
|
+
log_lines = []
|
|
213
|
+
|
|
214
|
+
blame_lines: list[str] | None = None
|
|
215
|
+
if current:
|
|
216
|
+
try:
|
|
217
|
+
completed = subprocess.run(
|
|
218
|
+
["git", "blame", "--line-porcelain", "-L", "1,40", str(path_obj)],
|
|
219
|
+
cwd=path_obj.parent if path_obj.parent.exists() else Path.cwd(),
|
|
220
|
+
capture_output=True,
|
|
221
|
+
text=True,
|
|
222
|
+
timeout=4,
|
|
223
|
+
check=False,
|
|
224
|
+
)
|
|
225
|
+
if completed.returncode == 0:
|
|
226
|
+
blame_lines = completed.stdout.splitlines()
|
|
227
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
228
|
+
blame_lines = None
|
|
229
|
+
|
|
230
|
+
return {"current": current, "log": log_lines, "blame": blame_lines}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def evaluate(
|
|
234
|
+
prompt_text: str,
|
|
235
|
+
target_path: str | Path,
|
|
236
|
+
old_text: str,
|
|
237
|
+
new_text: str,
|
|
238
|
+
*,
|
|
239
|
+
threshold: int = DEFAULT_THRESHOLD,
|
|
240
|
+
) -> dict:
|
|
241
|
+
"""Convenience wrapper used by hook_guardrails.
|
|
242
|
+
|
|
243
|
+
Returns a payload safe to JSON-serialize and embed in the deny reason
|
|
244
|
+
or the pass-through note.
|
|
245
|
+
"""
|
|
246
|
+
decision = check_diff(prompt_text, old_text, new_text, target_path, threshold=threshold)
|
|
247
|
+
payload = decision.to_payload()
|
|
248
|
+
payload["target_path"] = str(target_path)
|
|
249
|
+
if decision.decision == "block":
|
|
250
|
+
history = read_file_history(target_path)
|
|
251
|
+
payload["history_log"] = history["log"]
|
|
252
|
+
payload["current_excerpt"] = (history["current"][:400] + "…") if history["current"] else ""
|
|
253
|
+
return payload
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
__all__ = [
|
|
257
|
+
"DEFAULT_THRESHOLD",
|
|
258
|
+
"GuardDecision",
|
|
259
|
+
"PUNCTUAL_VERBS",
|
|
260
|
+
"UI_EXTENSIONS",
|
|
261
|
+
"check_diff",
|
|
262
|
+
"classify_request",
|
|
263
|
+
"count_changed_lines",
|
|
264
|
+
"evaluate",
|
|
265
|
+
"is_ui_path",
|
|
266
|
+
"read_file_history",
|
|
267
|
+
]
|