{"repo":"NousResearch/hermes-agent","public_id":"2648","display_label":"hermes update command broken across install methods","summary":"Multiple bugs and proposed fixes around `hermes update` failing under various installation scenarios, including uv tool installs, editable/PyPI installs, system Python, and Debian/Ubuntu PEP 668 restrictions due to incorrect pip or interpreter targeting.","subsystem":"hermes_cli","counts":{"members":17,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":6,"merged_prs":3,"attempt_prs":8},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":3099},"resolution":{"state":"partially_resolved","by":"pr-3099,pr-35224,pr-35248"},"freshness":{"state":"verified","last_verified_at":"2026-05-30T09:08:11+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.09676470588235293},"fix_edges":[{"issue_number":2648,"pr_number":3099,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":29700,"pr_number":35248,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":35031,"pr_number":35224,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":2648,"title":"Update script uses bare 'pip' instead of venv pip, fails on Debian/Ubuntu with externally-managed-environment error","url":null,"state":"closed"},{"kind":"issue","number":29700,"title":"`hermes update` fails when Hermes is installed via `uv tool install`","url":null,"state":"closed"},{"kind":"issue","number":35031,"title":"hermes update fails with 'No virtual environment found' when launcher venv is not active in shell","url":null,"state":"closed"},{"kind":"issue","number":35105,"title":"[Bug]: `hermes update` fails on a PyPI/editable install when `uv` is available","url":null,"state":"closed"},{"kind":"issue","number":39444,"title":"fix: `hermes update` fails with 'No virtual environment found' when using uv","url":null,"state":"closed"},{"kind":"issue","number":48721,"title":"[Bug]: hermes update on system Python targets wrong interpreter and hits PEP 668 (no-venv + uv branch)","url":null,"state":"closed"},{"kind":"pr","number":2655,"title":"fix: use sys.executable for pip in update script to fix PEP 668 (#2648)","url":null,"state":"closed","close_reason":{"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":2715,"title":"fix(cli): use venv pip in update command instead of system pip","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#3099","number":3099},{"kind":"pr","value":"#2655","number":2655}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":3099,"title":"fix: use sys.executable for pip in update commands to fix PEP 668","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/main.py"]},{"kind":"pr","number":29701,"title":"fix(cli): add --system flag to uv pip install for uv tool installs (#29700)","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":29703,"title":"fix(cli): use `uv tool upgrade` when Hermes is a uv tool install (#29700)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#35248","number":35248}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":29707,"title":"fix(cli): use 'uv tool upgrade' when hermes was installed via 'uv tool install' (#29700)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#35248","number":35248}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":35064,"title":"fix(update): export launcher virtualenv to uv (#35031)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#35224","number":35224}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":35224,"title":"fix(update): export launcher virtualenv to uv (#35031)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/main.py","tests/hermes_cli/test_cmd_update.py"],"fix_hunks":[{"file":"hermes_cli/main.py","hunks":["@@ -8929,7 +8929,10 @@ def _cmd_update_pip(args):\n         cmd = [sys.executable, \"-m\", \"pip\", \"install\", \"--upgrade\", \"hermes-agent\"]\n \n     print(f\"→ Running: {' '.join(cmd)}\")\n-    result = subprocess.run(cmd)\n+    run_kwargs = {}\n+    if sys.prefix != sys.base_prefix:\n+        run_kwargs[\"env\"] = {**os.environ, \"VIRTUAL_ENV\": sys.prefix}\n+    result = subprocess.run(cmd, **run_kwargs)\n     if result.returncode != 0:\n         print(\"✗ Update failed\")\n         sys.exit(1)"]},{"file":"tests/hermes_cli/test_cmd_update.py","hunks":["@@ -39,6 +39,45 @@ def mock_args():\n     return SimpleNamespace()\n \n \n+class TestCmdUpdatePip:\n+    \"\"\"Regression tests for pip-install update flows.\"\"\"\n+\n+    @patch(\"shutil.which\", return_value=\"/usr/bin/uv\")\n+    @patch(\"subprocess.run\")\n+    def test_update_pip_exports_virtualenv_from_sys_prefix(\n+        self, mock_run, _mock_which, mock_args, monkeypatch\n+    ):\n+        from hermes_cli import main as hm\n+\n+        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout=\"\", stderr=\"\")\n+        monkeypatch.delenv(\"VIRTUAL_ENV\", raising=False)\n+        monkeypatch.setattr(hm.sys, \"prefix\", \"/tmp/hermes-launcher-venv\")\n+        monkeypatch.setattr(hm.sys, \"base_prefix\", \"/usr\")\n+\n+        hm._cmd_update_pip(mock_args)\n+\n+        assert mock_run.call_count == 1\n+        assert mock_run.call_args.args[0] == [\"/usr/bin/uv\", \"pip\", \"install\", \"--upgrade\", \"hermes-agent\"]\n+        assert mock_run.call_args.kwargs[\"env\"][\"VIRTUAL_ENV\"] == \"/tmp/hermes-launcher-venv\"\n+\n+    @patch(\"shutil.which\", return_value=\"/usr/bin/uv\")\n+    @patch(\"subprocess.run\")\n+    def test_update_pip_does_not_export_virtualenv_for_system_python(\n+        self, mock_run, _mock_which, mock_args, monkeypatch\n+    ):\n+        from hermes_cli import main as hm\n+\n+        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout=\"\", stderr=\"\")\n+        monkeypatch.delenv(\"VIRTUAL_ENV\", raising=False)\n+        monkeypatch.setattr(hm.sys, \"prefix\", \"/usr\")\n+        monkeypatch.setattr(hm.sys, \"base_prefix\", \"/usr\")\n+\n+        hm._cmd_update_pip(mock_args)\n+\n+        assert mock_run.call_count == 1\n+        assert \"env\" not in mock_run.call_args.kwargs\n+\n+\n class TestCmdUpdateBranchFallback:\n     \"\"\"cmd_update falls back to main when current branch has no remote counterpart.\"\"\"\n \n"]}]},{"kind":"pr","number":35248,"title":"fix(update): handle uv-tool, pipx, and system-Python installs in hermes update (#29700)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/config.py","hermes_cli/main.py","scripts/release.py","tests/hermes_cli/test_uv_tool_update.py"],"fix_hunks":[{"file":"hermes_cli/config.py","hunks":["@@ -329,6 +329,34 @@ def stamp_install_method(method: str) -> None:\n         pass\n \n \n+def is_uv_tool_install() -> bool:\n+    \"\"\"Return True when the *running* Hermes lives in a ``uv tool`` layout.\n+\n+    ``uv tool install hermes-agent`` places the install at\n+    ``.../uv/tools/hermes-agent/...`` (default ``~/.local/share/uv/tools``,\n+    or ``$UV_TOOL_DIR/...``). Such installs live outside any virtualenv, so\n+    ``uv pip install`` fails with ``No virtual environment found`` and the\n+    update path must use ``uv tool upgrade`` instead.\n+\n+    Detection is intentionally restricted to properties of the running\n+    interpreter (``sys.prefix`` / ``sys.executable``). We deliberately do\n+    NOT consult ``uv tool list``: it would also return True when\n+    ``hermes-agent`` happens to be uv-tool-installed on the machine while\n+    the *active* Hermes is a regular pip/venv install, causing\n+    ``hermes update`` to upgrade the wrong copy. It would also block on a\n+    subprocess call (~seconds) just to compute a recommendation string.\n+    \"\"\"\n+    def _has_uv_tool_marker(path: str) -> bool:\n+        norm = os.path.normpath(path).replace(os.sep, \"/\").lower()\n+        return \"/uv/tools/hermes-agent/\" in norm + \"/\"\n+\n+    if _has_uv_tool_marker(sys.prefix):\n+        return True\n+    if _has_uv_tool_marker(sys.executable or \"\"):\n+        return True\n+    return False\n+\n+\n def recommended_update_command_for_method(method: str) -> str:\n     \"\"\"Return the update command or guidance for a given install method.\"\"\"\n     if method == \"nixos\":","@@ -338,9 +366,10 @@ def recommended_update_command_for_method(method: str) -> str:\n     if method == \"docker\":\n         return \"docker pull nousresearch/hermes-agent:latest\"\n     if method == \"pip\":\n+        if is_uv_tool_install():\n+            return \"uv tool upgrade hermes-agent\"\n         import shutil\n-        uv = shutil.which(\"uv\")\n-        if uv:\n+        if shutil.which(\"uv\"):\n             return \"uv pip install --upgrade hermes-agent\"\n         return \"pip install --upgrade hermes-agent\"\n     return \"hermes update\""]},{"file":"hermes_cli/main.py","hunks":["@@ -8971,19 +8971,49 @@ def cmd_update(args):\n def _cmd_update_pip(args):\n     \"\"\"Update Hermes via pip (for PyPI installs).\"\"\"\n     from hermes_cli import __version__\n+    from hermes_cli.config import is_uv_tool_install\n \n     print(f\"→ Current version: {__version__}\")\n     print(\"→ Checking PyPI for updates...\")\n \n     uv = shutil.which(\"uv\")\n-    if uv:\n+    in_venv = sys.prefix != sys.base_prefix\n+    # pipx-managed installs live under .../pipx/venvs/<name>/...\n+    pipx_managed = \"pipx\" in sys.prefix.split(os.sep)\n+    pipx = shutil.which(\"pipx\") if pipx_managed else None\n+\n+    # Only the ``uv pip install`` path inside a venv needs VIRTUAL_ENV\n+    # exported (uv refuses to install without it when the launcher shim\n+    # didn't activate the venv). ``uv tool upgrade`` / ``pipx upgrade``\n+    # operate on a named environment and ignore VIRTUAL_ENV, so we don't\n+    # set it for them.\n+    export_virtualenv = False\n+\n+    if is_uv_tool_install():\n+        if not uv:\n+            print(\"✗ Detected a uv-tool install but `uv` is not on PATH; install uv and retry.\")\n+            sys.exit(1)\n+        cmd = [uv, \"tool\", \"upgrade\", \"hermes-agent\"]\n+    elif pipx_managed and pipx:\n+        # pipx owns its own venv; ``pipx upgrade`` is the only correct path.\n+        # Matches scripts/auto-update.sh, which already uses pipx upgrade.\n+        cmd = [pipx, \"upgrade\", \"hermes-agent\"]\n+    elif uv:\n         cmd = [uv, \"pip\", \"install\", \"--upgrade\", \"hermes-agent\"]\n+        if in_venv:\n+            # Launcher shim runs the venv interpreter but doesn't export\n+            # VIRTUAL_ENV; without it uv errors \"No virtual environment found\".\n+            export_virtualenv = True\n+        else:\n+            # Outside any venv, ``--system`` lets uv target the active\n+            # interpreter, matching pip's default behaviour.\n+            cmd.insert(3, \"--system\")\n     else:\n         cmd = [sys.executable, \"-m\", \"pip\", \"install\", \"--upgrade\", \"hermes-agent\"]\n \n     print(f\"→ Running: {' '.join(cmd)}\")\n     run_kwargs = {}\n-    if sys.prefix != sys.base_prefix:\n+    if export_virtualenv:\n         run_kwargs[\"env\"] = {**os.environ, \"VIRTUAL_ENV\": sys.prefix}\n     result = subprocess.run(cmd, **run_kwargs)\n     if result.returncode != 0:"]},{"file":"scripts/release.py","hunks":["@@ -651,6 +651,7 @@\n     \"[redacted:email]\": \"alexzhu0\",\n     \"[redacted:email]\": \"AntAISecurityLab\",\n     \"[redacted:email]\": \"briandevans\",\n+    \"[redacted:email]\": \"inchargeautomation-lab\",\n     \"[redacted:email]\": \"Bartok9\",\n     \"[redacted:email]\": \"sweetcornna\",\n     \"[redacted:email]\": \"CruxExperts\","]},{"file":"tests/hermes_cli/test_uv_tool_update.py","hunks":["@@ -0,0 +1,311 @@\n+\"\"\"Tests for uv-tool install detection in the update path (issue #29700).\n+\n+``uv tool install hermes-agent`` lives outside any venv, so the previous\n+``uv pip install --upgrade`` update path failed with ``No virtual\n+environment found``. ``is_uv_tool_install`` should detect this layout and\n+both the user-facing recommended command and the actual\n+``_cmd_update_pip`` subprocess invocation should switch to\n+``uv tool upgrade hermes-agent``.\n+\n+Detection is restricted to properties of the running interpreter\n+(``sys.prefix`` / ``sys.executable``) so a pip/venv install on a machine\n+that also has ``uv tool install hermes-agent`` does not get misclassified.\n+\"\"\"\n+from __future__ import annotations\n+\n+import subprocess\n+from types import SimpleNamespace\n+from unittest.mock import patch\n+\n+import pytest\n+\n+\n+# ---------------------------------------------------------------------------\n+# is_uv_tool_install\n+# ---------------------------------------------------------------------------\n+\n+\n+class TestIsUvToolInstall:\n+    def test_returns_true_when_sys_prefix_matches_uv_tool_layout(self):\n+        from hermes_cli import config\n+\n+        with patch.object(config.sys, \"prefix\", \"/home/user/.local/share/uv/tools/hermes-agent\"):\n+            assert config.is_uv_tool_install() is True\n+\n+    def test_returns_true_when_sys_executable_matches_uv_tool_layout(self):\n+        \"\"\"Some uv-tool layouts surface the marker on ``sys.executable`` (bin/python).\"\"\"\n+        from hermes_cli import config\n+\n+        with patch.object(config.sys, \"prefix\", \"/some/unrelated/venv\"), \\\n+             patch.object(\n+                 config.sys,\n+                 \"executable\",\n+                 \"/home/user/.local/share/uv/tools/hermes-agent/bin/python\",\n+             ):\n+            assert config.is_uv_tool_install() is True\n+\n+    def test_returns_false_when_neither_prefix_nor_executable_matches(self):\n+        from hermes_cli import config\n+\n+        with patch.object(config.sys, \"prefix\", \"/some/unrelated/venv\"), \\\n+             patch.object(config.sys, \"executable\", \"/usr/bin/python3\"):\n+            assert config.is_uv_tool_install() is False\n+\n+    def test_does_not_consult_uv_tool_list(self):\n+        \"\"\"Detection must NOT shell out: ``uv tool list`` would false-positive\n+        when the active install is pip/venv but the machine also has\n+        ``uv tool install hermes-agent`` somewhere on disk. Copilot review on\n+        PR #29703 flagged this; the fix is to never call ``uv tool list``\n+        from the detection path.\"\"\"\n+        from hermes_cli import config\n+\n+        with patch.object(config.sys, \"prefix\", \"/some/unrelated/venv\"), \\\n+             patch.object(config.sys, \"executable\", \"/usr/bin/python3\"), \\\n+             patch(\"subprocess.run\") as mock_run:\n+            assert config.is_uv_tool_install() is False\n+            mock_run.assert_not_called()\n+\n+    def test_case_insensitive_match(self):\n+        \"\"\"Match must be case-insensitive — Windows paths preserve case\n+        (e.g. ``...AppData\\\\Local\\\\UV\\\\Tools\\\\hermes-agent``) and a case-sensitive\n+        check would miss them. We exercise the lower-cased compare path here\n+        without monkey-patching ``os.sep``, which would break the whole suite.\"\"\"\n+        from hermes_cli import config\n+\n+        with patch.object(\n+            config.sys, \"prefix\", \"/HOME/USER/.local/share/UV/Tools/hermes-agent\"\n+        ):\n+            assert config.is_uv_tool_install() is True\n+\n+    def test_handles_empty_executable(self):\n+        from hermes_cli import config\n+\n+        with patch.object(config.sys, \"prefix\", \"/some/unrelated/venv\"), \\\n+             patch.object(config.sys, \"executable\", \"\"):\n+            assert config.is_uv_tool_install() is False\n+\n+\n+# ---------------------------------------------------------------------------\n+# recommended_update_command_for_method\n+# ---------------------------------------------------------------------------\n+\n+\n+class TestRecommendedUpdateCommandForUvTool:\n+    def test_uv_tool_install_recommends_uv_tool_upgrade(self):\n+        from hermes_cli import config\n+\n+        with patch(\"shutil.which\", return_value=\"/usr/local/bin/uv\"), \\\n+             patch.object(config, \"is_uv_tool_install\", return_value=True):\n+            cmd = config.recommended_update_command_for_method(\"pip\")\n+            assert cmd == \"uv tool upgrade hermes-agent\"\n+\n+    def test_uv_tool_install_recommends_uv_tool_upgrade_even_without_uv_on_path(self):\n+        \"\"\"Recommendation reflects the *install method*, not whether ``uv`` is\n+        currently on PATH — the user needs to know the right command to run.\"\"\"\n+        from hermes_cli import config\n+\n+        with patch(\"shutil.which\", return_value=None), \\\n+             patch.object(config, \"is_uv_tool_install\", return_value=True):\n+            cmd = config.recommended_update_command_for_method(\"pip\")\n+            assert cmd == \"uv tool upgrade hermes-agent\"\n+\n+    def test_uv_pip_install_keeps_legacy_recommendation(self):\n+        \"\"\"Existing behavior: uv is on PATH but Hermes is a regular pip install.\"\"\"\n+        from hermes_cli import config\n+\n+        with patch(\"shutil.which\", return_value=\"/usr/local/bin/uv\"), \\\n+             patch.object(config, \"is_uv_tool_install\", return_value=False):\n+            cmd = config.recommended_update_command_for_method(\"pip\")\n+            assert cmd == \"uv pip install --upgrade hermes-agent\"\n+\n+    def test_no_uv_falls_back_to_plain_pip(self):\n+        from hermes_cli import config\n+\n+        with patch(\"shutil.which\", return_value=None), \\\n+             patch.object(config, \"is_uv_tool_install\", return_value=False):\n+            cmd = config.recommended_update_command_for_method(\"pip\")\n+            assert cmd == \"pip install --upgrade hermes-agent\"\n+\n+    def test_recommendation_does_not_spawn_subprocess(self):\n+        \"\"\"Computing the recommendation string must be cheap — no ``uv tool list``\n+        spawn. Copilot review on PR #29703 flagged the prior subprocess hop\n+        as adding overhead and a multi-second timeout window for what is\n+        purely a display string.\"\"\"\n+        from hermes_cli import config\n+\n+        with patch.object(config.sys, \"prefix\", \"/some/unrelated/venv\"), \\\n+             patch.object(config.sys, \"executable\", \"/usr/bin/python3\"), \\\n+             patch(\"shutil.which\", return_value=\"/usr/local/bin/uv\"), \\\n+             patch(\"subprocess.run\") as mock_run:\n+            cmd = config.recommended_update_command_for_method(\"pip\")\n+            mock_run.assert_not_called()\n+            assert cmd == \"uv pip install --upgrade hermes-agent\"\n+\n+\n+# ---------------------------------------------------------------------------\n+# _cmd_update_pip subprocess command\n+# ---------------------------------------------------------------------------\n+\n+\n+class TestCmdUpdatePipUsesUvTool:\n+    @patch(\"subprocess.run\")\n+    def test_runs_uv_tool_upgrade_when_uv_tool_install(self, mock_run):\n+        \"\"\"The actual subprocess invocation must switch to ``uv tool upgrade``.\"\"\"\n+        from hermes_cli.main import _cmd_update_pip\n+\n+        mock_run.return_value = subprocess.CompletedProcess([\"uv\"], 0, stdout=\"\", stderr=\"\")\n+        with patch(\"shutil.which\", return_value=\"/usr/local/bin/uv\"), \\\n+             patch(\"hermes_cli.config.is_uv_tool_install\", return_value=True):\n+            _cmd_update_pip(SimpleNamespace())\n+\n+        assert mock_run.call_args[0][0] == [\"/usr/local/bin/uv\", \"tool\", \"upgrade\", \"hermes-agent\"]\n+\n+    @patch(\"subprocess.run\")\n+    def test_runs_uv_pip_install_when_not_uv_tool(self, mock_run):\n+        \"\"\"Existing behavior preserved when uv is present but Hermes isn't a tool install.\"\"\"\n+        from hermes_cli.main import _cmd_update_pip\n+\n+        mock_run.return_value = subprocess.CompletedProcess([\"uv\"], 0, stdout=\"\", stderr=\"\")\n+        with patch(\"shutil.which\", return_value=\"/usr/local/bin/uv\"), \\\n+             patch(\"hermes_cli.config.is_uv_tool_install\", return_value=False):\n+            _cmd_update_pip(SimpleNamespace())\n+\n+        assert mock_run.call_args[0][0] == [\n+            \"/usr/local/bin/uv\",\n+            \"pip\",\n+            \"install\",\n+            \"--upgrade\",\n+            \"hermes-agent\",\n+        ]\n+\n+    @patch(\"subprocess.run\")\n+    def test_falls_back_to_pip_when_no_uv(self, mock_run):\n+        from hermes_cli.main import _cmd_update_pip\n+\n+        mock_run.return_value = subprocess.CompletedProcess([\"pip\"], 0, stdout=\"\", stderr=\"\")\n+        with patch(\"shutil.which\", return_value=None), \\\n+             patch(\"hermes_cli.config.is_uv_tool_install\", return_value=False):\n+            _cmd_update_pip(SimpleNamespace())\n+\n+        cmd = mock_run.call_args[0][0]\n+        assert cmd[1:] == [\"-m\", \"pip\", \"install\", \"--upgrade\", \"hermes-agent\"]\n+\n+    @patch(\"subprocess.run\")\n+    def test_exits_nonzero_on_subprocess_failure(self, mock_run):\n+        from hermes_cli.main import _cmd_update_pip\n+\n+        mock_run.return_value = subprocess.CompletedProcess([\"uv\"], 1, stdout=\"\", stderr=\"\")\n+        with patch(\"shutil.which\", return_value=\"/usr/local/bin/uv\"), \\\n+             patch(\"hermes_cli.config.is_uv_tool_install\", return_value=True):\n+            with pytest.raises(SystemExit) as exc_info:\n+                _cmd_update_pip(SimpleNamespace())\n+        assert exc_info.value.code == 1\n+\n+    @patch(\"subprocess.run\")\n+    def test_uv_tool_install_without_uv_on_path_exits_with_hint(self, mock_run):\n+        \"\"\"If the running interpreter looks like a uv-tool install but ``uv`` is\n+        somehow missing from PATH, surface a clear hint instead of silently\n+        falling back to ``python -m pip``, which would either fail (no venv)\n+        or upgrade the wrong copy.\"\"\"\n+        from hermes_cli.main import _cmd_update_pip\n+\n+        with patch(\"shutil.which\", return_value=None), \\\n+             patch(\"hermes_cli.config.is_uv_tool_install\", return_value=True):\n+            with pytest.raises(SystemExit) as exc_info:\n+                _cmd_update_pip(SimpleNamespace())\n+        assert exc_info.value.code == 1\n+        mock_run.assert_not_called()\n+\n+\n+# ---------------------------------------------------------------------------\n+# pipx-managed installs, --system fallback, and VIRTUAL_ENV overlay\n+# (issue #29700 / #35031 family — consolidated update-path handling)\n+# ---------------------------------------------------------------------------\n+\n+\n+class TestCmdUpdatePipInstallLayouts:\n+    \"\"\"The uv pip path must adapt to where the running interpreter lives:\n+\n+    - inside a venv (launcher shim)  -> export VIRTUAL_ENV, no ``--system``\n+    - bare pip outside any venv      -> add ``--system``, no overlay\n+    - pipx-managed                   -> ``pipx upgrade``\n+    \"\"\"\n+\n+    @patch(\"subprocess.run\")\n+    def test_pipx_managed_uses_pipx_upgrade(self, mock_run, monkeypatch):\n+        from hermes_cli import main as hm\n+\n+        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout=\"\", stderr=\"\")\n+        monkeypatch.setattr(hm.sys, \"prefix\", \"/home/u/.local/pipx/venvs/hermes-agent\")\n+        monkeypatch.setattr(hm.sys, \"base_prefix\", \"/usr\")\n+\n+        def _which(name):\n+            return {\"uv\": \"/usr/bin/uv\", \"pipx\": \"/usr/bin/pipx\"}.get(name)\n+\n+        with patch(\"shutil.which\", side_effect=_which), \\\n+             patch(\"hermes_cli.config.is_uv_tool_install\", return_value=False):\n+            hm._cmd_update_pip(SimpleNamespace())\n+\n+        assert mock_run.call_args[0][0] == [\"/usr/bin/pipx\", \"upgrade\", \"hermes-agent\"]\n+        # pipx upgrade ignores VIRTUAL_ENV; we must not set it.\n+        assert \"env\" not in mock_run.call_args.kwargs\n+\n+    @patch(\"subprocess.run\")\n+    def test_pipx_layout_without_pipx_binary_treated_as_venv(\n+        self, mock_run, monkeypatch\n+    ):\n+        from hermes_cli import main as hm\n+\n+        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout=\"\", stderr=\"\")\n+        monkeypatch.setattr(hm.sys, \"prefix\", \"/home/u/.local/pipx/venvs/hermes-agent\")\n+        monkeypatch.setattr(hm.sys, \"base_prefix\", \"/usr\")\n+\n+        # pipx layout detected via prefix, but pipx binary missing on PATH.\n+        def _which(name):\n+            return \"/usr/bin/uv\" if name == \"uv\" else None\n+\n+        with patch(\"shutil.which\", side_effect=_which), \\\n+             patch(\"hermes_cli.config.is_uv_tool_install\", return_value=False):\n+            hm._cmd_update_pip(SimpleNamespace())\n+\n+        # prefix != base_prefix, so this is treated as a venv -> overlay, no --system.\n+        assert mock_run.call_args[0][0] == [\n+            \"/usr/bin/uv\", \"pip\", \"install\", \"--upgrade\", \"hermes-agent\",\n+        ]\n+        assert mock_run.call_args.kwargs[\"env\"][\"VIRTUAL_ENV\"].endswith(\"hermes-agent\")\n+\n+    @patch(\"subprocess.run\")\n+    def test_bare_pip_outside_venv_adds_system(self, mock_run, monkeypatch):\n+        from hermes_cli import main as hm\n+\n+        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout=\"\", stderr=\"\")\n+        # No venv: prefix == base_prefix.\n+        monkeypatch.setattr(hm.sys, \"prefix\", \"/usr\")\n+        monkeypatch.setattr(hm.sys, \"base_prefix\", \"/usr\")\n+\n+        with patch(\"shutil.which\", return_value=\"/usr/bin/uv\"), \\\n+             patch(\"hermes_cli.config.is_uv_tool_install\", return_value=False):\n+            hm._cmd_update_pip(SimpleNamespace())\n+\n+        assert mock_run.call_args[0][0] == [\n+            \"/usr/bin/uv\", \"pip\", \"install\", \"--system\", \"--upgrade\", \"hermes-agent\",\n+        ]\n+        assert \"env\" not in mock_run.call_args.kwargs\n+\n+    @patch(\"subprocess.run\")\n+    def test_venv_exports_virtualenv_and_omits_system(self, mock_run, monkeypatch):\n+        from hermes_cli import main as hm\n+\n+        mock_run.return_value = subprocess.CompletedProcess([], 0, stdout=\"\", stderr=\"\")\n+        monkeypatch.delenv(\"VIRTUAL_ENV\", raising=False)\n+        monkeypatch.setattr(hm.sys, \"prefix\", \"/home/u/.hermes/hermes-agent/venv\")\n+        monkeypatch.setattr(hm.sys, \"base_prefix\", \"/usr\")\n+\n+        with patch(\"shutil.which\", return_value=\"/usr/bin/uv\"), \\\n+             patch(\"hermes_cli.config.is_uv_tool_install\", return_value=False):\n+            hm._cmd_update_pip(SimpleNamespace())\n+\n+        cmd = mock_run.call_args[0][0]\n+        assert \"--system\" not in cmd\n+        assert cmd == [\"/usr/bin/uv\", \"pip\", \"install\", \"--upgrade\", \"hermes-agent\"]\n+        assert mock_run.call_args.kwargs[\"env\"][\"VIRTUAL_ENV\"] == \"/home/u/.hermes/hermes-agent/venv\"\n"]}]},{"kind":"pr","number":48726,"title":"fix(cli): pin --python and add --break-system-packages for no-venv uv update","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48721","number":48721},{"kind":"pr","value":"#48721","number":48721}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":48756,"title":"fix(cli): target correct Python in no-venv uv update path (#48721)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48726","number":48726},{"kind":"pr","value":"#48721","number":48721}],"category":"duplicate_of","implication":"duplicate"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}