diff --git a/tests/tools/test_self_repo_guard.py b/tests/tools/test_self_repo_guard.py index c0f9ae9dc0..06d96b4bea 100644 --- a/tests/tools/test_self_repo_guard.py +++ b/tests/tools/test_self_repo_guard.py @@ -354,3 +354,28 @@ class TestUnparseableCommands: def test_subshell_syntax_does_not_crash(self, repo): hit, _ = _detect("VAL=$(git rev-parse HEAD) git checkout main", repo, repo) assert hit is True + + +class TestBlockMessageGuidance: + """The block message must steer agents to a disk-backed scratch clone, + not a bare "temporary clone" (agents defaulted to /tmp, which is tmpfs + on most distros — parallel salvage clones running npm ci filled a 32GB + tmpfs to 97% in one campaign).""" + + def test_message_recommends_shared_clone_on_disk(self, repo): + hit, msg = _detect("git rebase origin/main", repo, repo) + assert hit is True + assert "git clone --shared" in msg + assert "scratch" in msg + + def test_message_warns_against_tmp_for_dep_installs(self, repo): + hit, msg = _detect("git rebase origin/main", repo, repo) + assert hit is True + assert "tmpfs" in msg + assert "Delete the clone" in msg + + def test_scratch_hint_honors_hermes_home(self, repo, monkeypatch): + monkeypatch.setenv("HERMES_HOME", "/custom/hermes-home") + hit, msg = _detect("git rebase origin/main", repo, repo) + assert hit is True + assert "/custom/hermes-home/scratch" in msg diff --git a/tools/self_repo_guard.py b/tools/self_repo_guard.py index 218367adff..aa92df2811 100644 --- a/tools/self_repo_guard.py +++ b/tools/self_repo_guard.py @@ -714,9 +714,22 @@ def detect_self_repo_git_mutation( def _block_message(operation: str, root: Path) -> str: + scratch = _scratch_dir_hint() return ( f"Blocked: `{operation}` would rewrite Hermes's live source checkout " f"({root}) and can mix module versions in this running process. " - "Use a separate worktree or temporary clone. To change this checkout, " - "stop Hermes, run the command externally, then restart Hermes." + f"Use a separate worktree or a shared clone on real disk, e.g. " + f"`git clone --shared {root} {scratch}/` — avoid /tmp for " + "clones that install node/python deps: /tmp is usually RAM-backed " + "tmpfs and a few dependency installs can fill it and ENOSPC other " + "work. Delete the clone when the branch is pushed. To change this " + "checkout, stop Hermes, run the command externally, then restart " + "Hermes." ) + + +def _scratch_dir_hint() -> str: + """Disk-backed scratch location suggested to agents for temporary clones.""" + hermes_home = os.environ.get("HERMES_HOME", "").strip() + base = Path(hermes_home).expanduser() if hermes_home else Path.home() / ".hermes" + return str(base / "scratch")