From 06dc51d62d0d5242fffab14fb2b65000ac3d280d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 9 Sep 2026 01:52:28 -0700 Subject: [PATCH] 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). --- agent/verification_evidence.py | 16 +++++++ tests/agent/test_verification_evidence.py | 6 +++ .../test_verification_evidence_disabled.py | 42 +++++++++++++++++++ .../test_verification_evidence_fd_leak.py | 6 +++ tests/agent/test_verification_stop.py | 7 ++++ tests/test_tui_gateway_server.py | 2 + .../test_ledger_and_nudge_integration.py | 6 +++ website/docs/user-guide/configuration.md | 2 + 8 files changed, 87 insertions(+) create mode 100644 tests/agent/test_verification_evidence_disabled.py diff --git a/agent/verification_evidence.py b/agent/verification_evidence.py index 468deb2a9e..69e7f2c1aa 100644 --- a/agent/verification_evidence.py +++ b/agent/verification_evidence.py @@ -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} diff --git a/tests/agent/test_verification_evidence.py b/tests/agent/test_verification_evidence.py index aae41eda91..566e63890f 100644 --- a/tests/agent/test_verification_evidence.py +++ b/tests/agent/test_verification_evidence.py @@ -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( diff --git a/tests/agent/test_verification_evidence_disabled.py b/tests/agent/test_verification_evidence_disabled.py new file mode 100644 index 0000000000..b48ce7a3f3 --- /dev/null +++ b/tests/agent/test_verification_evidence_disabled.py @@ -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() diff --git a/tests/agent/test_verification_evidence_fd_leak.py b/tests/agent/test_verification_evidence_fd_leak.py index e973aabb9e..a2905079eb 100644 --- a/tests/agent/test_verification_evidence_fd_leak.py +++ b/tests/agent/test_verification_evidence_fd_leak.py @@ -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. diff --git a/tests/agent/test_verification_stop.py b/tests/agent/test_verification_stop.py index f0d221a50d..25b163106a 100644 --- a/tests/agent/test_verification_stop.py +++ b/tests/agent/test_verification_stop.py @@ -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. diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 8b2ece07b8..4a15d7b257 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -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 diff --git a/tests/verify/test_ledger_and_nudge_integration.py b/tests/verify/test_ledger_and_nudge_integration.py index cb9654e533..e993aaea8e 100644 --- a/tests/verify/test_ledger_and_nudge_integration.py +++ b/tests/verify/test_ledger_and_nudge_integration.py @@ -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( diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index a611274553..906c1a89fe 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -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`)