fix: verification evidence ledger is inert while verify_on_stop is off

The ledger in verification_evidence.db exists only to feed the verify-on-stop
guard, but the recorder kept running on every foreground terminal command and
every file edit after #53552 turned the guard off by default. Users who never
opted in still accumulated a multi-MB database (7 MB / 4.6k rows on one install).

Every ledger entry point (record_terminal_result, record_verify_run,
mark_workspace_edited, verification_status) now checks verify_on_stop_enabled()
first and returns without opening or creating the database when the guard is
off. verification_status reports {"status": "disabled"} in that case; no client
consumes the verification.status RPC yet, so nothing downstream changes.

Existing ledger tests pin HERMES_VERIFY_ON_STOP=1 since they exercise the ledger
itself; the new test proves the off path never creates the file (red on base).
This commit is contained in:
Teknium
2026-09-09 01:52:28 -07:00
parent 677e8ed8a4
commit 06dc51d62d
8 changed files with 87 additions and 0 deletions
+16
View File
@@ -111,6 +111,14 @@ def _db_path() -> Path:
return get_hermes_home() / "verification_evidence.db"
def _ledger_enabled() -> bool:
"""The ledger exists only to feed verify-on-stop; when that guard is off nothing may
record, read, or even create the database (an unconsumed ledger is pure disk churn)."""
from agent.verification_stop import verify_on_stop_enabled
return verify_on_stop_enabled()
def _connect() -> sqlite3.Connection:
from hermes_state_wal import apply_wal_with_fallback
@@ -451,6 +459,8 @@ def record_terminal_result(
*, command: str, cwd: str | Path | None, session_id: str | None, exit_code: int, output: str = ""
) -> Optional[dict[str, Any]]:
"""Record a foreground terminal result when it is verification evidence."""
if not _ledger_enabled():
return None
evidence = classify_verification_command(command, cwd=cwd, session_id=session_id, exit_code=exit_code, output=output)
return None if evidence is None else _insert_evidence(evidence)
@@ -465,6 +475,8 @@ def record_verify_run(
canonical test command would. ``root`` is re-resolved through project facts
so it matches what :func:`verification_status` derives later.
"""
if not _ledger_enabled():
return None
resolved = str(Path(root).resolve())
return _insert_evidence(VerificationEvidence(
command=command, canonical_command="hermes verify", kind="verify",
@@ -511,6 +523,8 @@ def mark_workspace_edited(
*, session_id: str | None, cwd: str | Path | None, paths: list[str] | tuple[str, ...] | None = None
) -> Optional[dict[str, Any]]:
"""Mark verification evidence stale after a successful file edit."""
if not _ledger_enabled():
return None
facts = _project_facts(cwd)
if not facts:
return None
@@ -547,6 +561,8 @@ def verification_status(*, session_id: str | None, cwd: str | Path | None) -> di
Evidence recorded before the latest edit is reported as ``stale``.
"""
if not _ledger_enabled():
return {"status": "disabled", "evidence": None}
facts = _project_facts(cwd)
if not facts:
return {"status": "not_applicable", "evidence": None}
@@ -13,6 +13,12 @@ from agent.verification_evidence import (
verification_status,
)
@pytest.fixture(autouse=True)
def _ledger_on(monkeypatch):
"""The ledger is inert unless verify-on-stop is enabled; these tests exercise the ledger."""
monkeypatch.setenv("HERMES_VERIFY_ON_STOP", "1")
def _node_project(root: Path) -> None:
(root / "package.json").write_text(
@@ -0,0 +1,42 @@
"""With verify-on-stop off (the default) the evidence ledger must be fully inert: no
recording, no staleness tracking, and no ``verification_evidence.db`` ever created.
The ledger's only consumer is the stop guard; an unconsumed ledger is disk churn."""
from pathlib import Path
import pytest
from agent.verification_evidence import (
mark_workspace_edited,
record_terminal_result,
record_verify_run,
verification_status,
)
@pytest.fixture
def project(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
monkeypatch.delenv("HERMES_VERIFY_ON_STOP", raising=False)
root = tmp_path / "project"
root.mkdir()
(root / "package.json").write_text('{"scripts": {"test": "vitest"}}', encoding="utf-8")
return root
def test_disabled_guard_never_touches_the_ledger(project, tmp_path):
db = tmp_path / ".hermes" / "verification_evidence.db"
assert record_terminal_result(command="npm test", cwd=project, session_id="s1", exit_code=0) is None
assert mark_workspace_edited(session_id="s1", cwd=project, paths=[str(project / "app.ts")]) is None
assert record_verify_run(root=project, session_id="s1", ok=True) is None
assert verification_status(session_id="s1", cwd=project)["status"] == "disabled"
assert not db.exists()
def test_enabled_guard_records_into_the_ledger(project, tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_VERIFY_ON_STOP", "1")
assert record_terminal_result(command="npm test", cwd=project, session_id="s1", exit_code=0)
assert verification_status(session_id="s1", cwd=project)["status"] == "passed"
assert Path(tmp_path / ".hermes" / "verification_evidence.db").exists()
@@ -13,6 +13,12 @@ import pytest
from agent import verification_evidence as ve
@pytest.fixture(autouse=True)
def _ledger_on(monkeypatch):
"""The ledger is inert unless verify-on-stop is enabled; these tests exercise the ledger."""
monkeypatch.setenv("HERMES_VERIFY_ON_STOP", "1")
class _TrackingConnection:
"""Delegates to a real sqlite3.Connection while recording close() calls.
+7
View File
@@ -28,6 +28,13 @@ def _make_project(root: Path) -> None:
_node_project(root)
@pytest.fixture(autouse=True)
def _ledger_on(monkeypatch):
"""The ledger is inert unless verify-on-stop is enabled; ``clear_verify_env`` (requested
explicitly, so it runs after this) strips it again for the enabled()-logic tests."""
monkeypatch.setenv("HERMES_VERIFY_ON_STOP", "1")
@pytest.fixture
def clear_verify_env(monkeypatch):
"""Clear every env signal verify_on_stop_enabled consults.
+2
View File
@@ -17018,6 +17018,7 @@ def test_session_most_recent_handles_db_unavailable(monkeypatch):
def test_verification_status_returns_recorded_evidence(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_VERIFY_ON_STOP", "1") # ledger is inert when the guard is off
profile_home = tmp_path / "profiles" / "verify"
profile_home.mkdir(parents=True)
monkeypatch.setattr(server, "_profile_home", lambda p: profile_home if p == "verify" else None)
@@ -17058,6 +17059,7 @@ def test_verification_status_returns_recorded_evidence(tmp_path, monkeypatch):
def test_verification_status_outside_workspace_is_not_applicable(monkeypatch, tmp_path):
monkeypatch.setenv("HERMES_VERIFY_ON_STOP", "1") # ledger is inert when the guard is off
# A cwd with no project facts (outside any code workspace) must report
# not_applicable. Force the "no facts" precondition rather than relying on
# tmp_path's ancestors being pristine — a stray marker file in a shared
@@ -23,6 +23,12 @@ from agent.verification_evidence import (
from agent.verification_stop import build_verify_on_stop_nudge
from hermes_cli.verify_cmd import run_verify_command
@pytest.fixture(autouse=True)
def _ledger_on(monkeypatch):
"""The ledger is inert unless verify-on-stop is enabled; these tests exercise the ledger."""
monkeypatch.setenv("HERMES_VERIFY_ON_STOP", "1")
def make_args(path, **overrides):
defaults = dict(
+2
View File
@@ -1179,6 +1179,8 @@ agent:
`verify_on_stop` accepts `true` (on everywhere), `false` (off — the default), or `"auto"` (legacy surface-aware behavior: on for interactive coding surfaces — CLI, TUI, desktop — and programmatic callers; off for messaging surfaces like Telegram/Discord where the verification narrative reads as chat noise). Off is the default everywhere: fresh installs ship `false` and the config migration turned it off on existing installs, so enabling it is an explicit opt-in. The `HERMES_VERIFY_ON_STOP` env var overrides the config value when set.
The evidence that feeds this guard (which test/lint/build commands ran, which files were edited since) lives in `~/.hermes/verification_evidence.db`. That ledger is only written or created while the guard is enabled; with `verify_on_stop: false` nothing is recorded and an existing file can be deleted freely.
For a user/plugin policy gate at the same point — keep the agent going with your own checks — see the [`pre_verify` hook](/user-guide/features/hooks#pre_verify).
## Standing Goals (`/goal`)