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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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`)
|
||||
|
||||
Reference in New Issue
Block a user