{"repo":"NousResearch/hermes-agent","public_id":"4672","display_label":"Gateway overwriting computed TERMINAL_CWD with raw config value","summary":"Most members (an issue and two PRs) concern gateway/run.py overwriting an already-computed `TERMINAL_CWD` with the raw config value such as '.'. One broader PR also covers making config.yaml the sole CWD source, deprecating .env CWD variables, and adding a memory reset command.","subsystem":"gateway","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":11029,"merged_at":"2026-04-16T13:48:34+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-04-16T13:48:34+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":4672,"pr_number":11029,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-16T13:48:34+00:00"},{"issue_number":10225,"pr_number":11029,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-16T13:48:34+00:00"}],"members":[{"kind":"issue","number":4672,"title":"gateway/run.py clobbers resolved TERMINAL_CWD with raw config value '.'","url":null,"state":"closed"},{"kind":"pr","number":4673,"title":"fix(gateway): don't clobber resolved TERMINAL_CWD with raw config value","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#4731","number":4731},{"kind":"pr","value":"#4669","number":4669},{"kind":"pr","value":"#4672","number":4672}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":4971,"title":"fix(gateway): don't clobber resolved TERMINAL_CWD with raw config","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3c42064efcd031971224e6aaea19fcdce9a97cfb","number":0},{"kind":"pr","value":"#11029","number":11029}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":11029,"title":"fix: enforce config.yaml as sole CWD source + deprecate .env CWD vars + add hermes memory reset","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","gateway/run.py","hermes_cli/config.py","hermes_cli/main.py","tests/cli/test_cwd_env_respect.py","tests/gateway/test_config_cwd_bridge.py","tests/hermes_cli/test_deprecated_cwd_warning.py","tests/hermes_cli/test_memory_reset.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -401,14 +401,27 @@ def load_cli_config() -> Dict[str, Any]:\n     # filesystem is directly accessible.  For ALL remote/container backends\n     # (ssh, docker, modal, singularity), the host path doesn't exist on the\n     # target -- remove the key so terminal_tool.py uses its per-backend default.\n-    if terminal_config.get(\"cwd\") in (\".\", \"auto\", \"cwd\"):\n-        effective_backend = terminal_config.get(\"env_type\", \"local\")\n-        if effective_backend == \"local\":\n-            terminal_config[\"cwd\"] = os.getcwd()\n-            defaults[\"terminal\"][\"cwd\"] = terminal_config[\"cwd\"]\n+    #\n+    # GUARD: If TERMINAL_CWD is already set to a real absolute path (by the\n+    # gateway's config bridge earlier in the process), don't clobber it.\n+    # This prevents a lazy import of cli.py during gateway runtime from\n+    # rewriting TERMINAL_CWD to the service's working directory.\n+    # See issue #10817.\n+    _CWD_PLACEHOLDERS = (\".\", \"auto\", \"cwd\")\n+    if terminal_config.get(\"cwd\") in _CWD_PLACEHOLDERS:\n+        _existing_cwd = os.environ.get(\"TERMINAL_CWD\", \"\")\n+        if _existing_cwd and _existing_cwd not in _CWD_PLACEHOLDERS and os.path.isabs(_existing_cwd):\n+            # Gateway (or earlier startup) already resolved a real path — keep it\n+            terminal_config[\"cwd\"] = _existing_cwd\n+            defaults[\"terminal\"][\"cwd\"] = _existing_cwd\n         else:\n-            # Remove so TERMINAL_CWD stays unset → tool picks backend default\n-            terminal_config.pop(\"cwd\", None)\n+            effective_backend = terminal_config.get(\"env_type\", \"local\")\n+            if effective_backend == \"local\":\n+                terminal_config[\"cwd\"] = os.getcwd()\n+                defaults[\"terminal\"][\"cwd\"] = terminal_config[\"cwd\"]\n+            else:\n+                # Remove so TERMINAL_CWD stays unset → tool picks backend default\n+                terminal_config.pop(\"cwd\", None)\n     \n     env_mappings = {\n         \"env_type\": \"TERMINAL_ENV\","]},{"file":"gateway/run.py","hunks":["@@ -131,6 +131,12 @@ def _ensure_ssl_certs() -> None:\n             for _cfg_key, _env_var in _terminal_env_map.items():\n                 if _cfg_key in _terminal_cfg:\n                     _val = _terminal_cfg[_cfg_key]\n+                    # Skip cwd placeholder values (\".\", \"auto\", \"cwd\") — the\n+                    # gateway resolves these to Path.home() later (line ~255).\n+                    # Writing the raw placeholder here would just be noise.\n+                    # Only bridge explicit absolute paths from config.yaml.\n+                    if _cfg_key == \"cwd\" and str(_val) in (\".\", \"auto\", \"cwd\"):\n+                        continue\n                     if isinstance(_val, list):\n                         os.environ[_env_var] = json.dumps(_val)\n                     else:","@@ -225,6 +231,13 @@ def _ensure_ssl_certs() -> None:\n except Exception:\n     pass\n \n+# Warn if user has deprecated MESSAGING_CWD / TERMINAL_CWD in .env\n+try:\n+    from hermes_cli.config import warn_deprecated_cwd_env_vars\n+    warn_deprecated_cwd_env_vars()\n+except Exception:\n+    pass\n+\n # Gateway runs in quiet mode - suppress debug output and use cwd directly (no temp dirs)\n os.environ[\"HERMES_QUIET\"] = \"1\"\n ","@@ -232,12 +245,14 @@ def _ensure_ssl_certs() -> None:\n os.environ[\"HERMES_EXEC_ASK\"] = \"1\"\n \n # Set terminal working directory for messaging platforms.\n-# If the user set an explicit path in config.yaml (not \".\" or \"auto\"),\n-# respect it. Otherwise use MESSAGING_CWD or default to home directory.\n+# config.yaml terminal.cwd is the canonical source (bridged to TERMINAL_CWD\n+# by the config bridge above).  When it's unset or a placeholder, default\n+# to home directory.  MESSAGING_CWD is accepted as a backward-compat\n+# fallback (deprecated — the warning above tells users to migrate).\n _configured_cwd = os.environ.get(\"TERMINAL_CWD\", \"\")\n if not _configured_cwd or _configured_cwd in (\".\", \"auto\", \"cwd\"):\n-    messaging_cwd = os.getenv(\"MESSAGING_CWD\") or str(Path.home())\n-    os.environ[\"TERMINAL_CWD\"] = messaging_cwd\n+    _fallback = os.getenv(\"MESSAGING_CWD\") or str(Path.home())\n+    os.environ[\"TERMINAL_CWD\"] = _fallback\n \n from gateway.config import (\n     Platform,","@@ -3403,7 +3418,7 @@ async def _prepare_inbound_message_text(\n                 from agent.context_references import preprocess_context_references_async\n                 from agent.model_metadata import get_model_context_length\n \n-                _msg_cwd = os.environ.get(\"MESSAGING_CWD\", os.path.expanduser(\"~\"))\n+                _msg_cwd = os.environ.get(\"TERMINAL_CWD\", os.path.expanduser(\"~\"))\n                 _msg_ctx_len = get_model_context_length(\n                     self._model,\n                     base_url=self._base_url or \"\",","@@ -5620,7 +5635,7 @@ async def _handle_rollback_command(self, event: MessageEvent) -> str:\n             max_snapshots=cp_cfg.get(\"max_snapshots\", 50),\n         )\n \n-        cwd = os.getenv(\"MESSAGING_CWD\", str(Path.home()))\n+        cwd = os.getenv(\"TERMINAL_CWD\", str(Path.home()))\n         arg = event.get_command_args().strip()\n \n         if not arg:"]},{"file":"hermes_cli/config.py","hunks":["@@ -1597,13 +1597,8 @@ def _ensure_hermes_home_managed(home: Path):\n     },\n \n     # ── Agent settings ──\n-    \"MESSAGING_CWD\": {\n-        \"description\": \"Working directory for terminal commands via messaging\",\n-        \"prompt\": \"Messaging working directory (default: home)\",\n-        \"url\": None,\n-        \"password\": [redacted:secret-assignment]\n-        \"category\": \"setting\",\n-    },\n+    # NOTE: MESSAGING_CWD was removed here — use terminal.cwd in config.yaml\n+    # instead.  The gateway reads TERMINAL_CWD (bridged from terminal.cwd).\n     \"SUDO_PASSWORD\": {\n         \"description\": \"Sudo password for terminal commands requiring root access; set to an explicit empty string to try empty without prompting\",\n         \"prompt\": \"Sudo password\",","@@ -2082,6 +2077,52 @@ def print_config_warnings(config: Optional[Dict[str, Any]] = None) -> None:\n     sys.stderr.write(\"\\n\".join(lines) + \"\\n\\n\")\n \n \n+def warn_deprecated_cwd_env_vars(config: Optional[Dict[str, Any]] = None) -> None:\n+    \"\"\"Warn if MESSAGING_CWD or TERMINAL_CWD is set in .env instead of config.yaml.\n+\n+    These env vars are deprecated — the canonical setting is terminal.cwd\n+    in config.yaml.  Prints a migration hint to stderr.\n+    \"\"\"\n+    import os, sys\n+    messaging_cwd = os.environ.get(\"MESSAGING_CWD\")\n+    terminal_cwd_env = os.environ.get(\"TERMINAL_CWD\")\n+\n+    if config is None:\n+        try:\n+            config = load_config()\n+        except Exception:\n+            return\n+\n+    terminal_cfg = config.get(\"terminal\", {})\n+    config_cwd = terminal_cfg.get(\"cwd\", \".\") if isinstance(terminal_cfg, dict) else \".\"\n+    # Only warn if config.yaml doesn't have an explicit path\n+    config_has_explicit_cwd = config_cwd not in (\".\", \"auto\", \"cwd\", \"\")\n+\n+    lines: list[str] = []\n+    if messaging_cwd:\n+        lines.append(\n+            f\"  \\033[33m⚠\\033[0m MESSAGING_CWD={messaging_cwd} found in .env — \"\n+            f\"this is deprecated.\"\n+        )\n+    if terminal_cwd_env and not config_has_explicit_cwd:\n+        # TERMINAL_CWD in env but not from config bridge — likely from .env\n+        lines.append(\n+            f\"  \\033[33m⚠\\033[0m TERMINAL_CWD={terminal_cwd_env} found in .env — \"\n+            f\"this is deprecated.\"\n+        )\n+    if lines:\n+        hint_path = os.environ.get(\"HERMES_HOME\", \"~/.hermes\")\n+        lines.insert(0, \"\\033[33m⚠ Deprecated .env settings detected:\\033[0m\")\n+        lines.append(\n+            f\"  \\033[2mMove to config.yaml instead:  \"\n+            f\"terminal:\\\\n    cwd: /your/project/path\\033[0m\"\n+        )\n+        lines.append(\n+            f\"  \\033[2mThen remove the old entries from {hint_path}/.env\\033[0m\"\n+        )\n+        sys.stderr.write(\"\\n\".join(lines) + \"\\n\\n\")\n+\n+\n def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, Any]:\n     \"\"\"\n     Migrate config to latest version, prompting for new required fields."]},{"file":"hermes_cli/main.py","hunks":["@@ -5659,6 +5659,18 @@ def cmd_plugins(args):\n     memory_sub.add_parser(\"setup\", help=\"Interactive provider selection and configuration\")\n     memory_sub.add_parser(\"status\", help=\"Show current memory provider config\")\n     memory_sub.add_parser(\"off\", help=\"Disable external provider (built-in only)\")\n+    _reset_parser = memory_sub.add_parser(\n+        \"reset\",\n+        help=\"Erase all built-in memory (MEMORY.md and USER.md)\",\n+    )\n+    _reset_parser.add_argument(\n+        \"--yes\", \"-y\", action=\"store_true\",\n+        help=\"Skip confirmation prompt\",\n+    )\n+    _reset_parser.add_argument(\n+        \"--target\", choices=[\"all\", \"memory\", \"user\"], default=\"all\",\n+        help=\"Which store to reset: 'all' (default), 'memory', or 'user'\",\n+    )\n \n     def cmd_memory(args):\n         sub = getattr(args, \"memory_command\", None)","@@ -5671,6 +5683,44 @@ def cmd_memory(args):\n             save_config(config)\n             print(\"\\n  ✓ Memory provider: built-in only\")\n             print(\"  Saved to config.yaml\\n\")\n+        elif sub == \"reset\":\n+            from hermes_constants import get_hermes_home, display_hermes_home\n+            mem_dir = get_hermes_home() / \"memories\"\n+            target = getattr(args, \"target\", \"all\")\n+            files_to_reset = []\n+            if target in (\"all\", \"memory\"):\n+                files_to_reset.append((\"MEMORY.md\", \"agent notes\"))\n+            if target in (\"all\", \"user\"):\n+                files_to_reset.append((\"USER.md\", \"user profile\"))\n+\n+            # Check what exists\n+            existing = [(f, desc) for f, desc in files_to_reset if (mem_dir / f).exists()]\n+            if not existing:\n+                print(f\"\\n  Nothing to reset — no memory files found in {display_hermes_home()}/memories/\\n\")\n+                return\n+\n+            print(f\"\\n  This will permanently erase the following memory files:\")\n+            for f, desc in existing:\n+                path = mem_dir / f\n+                size = path.stat().st_size\n+                print(f\"    ◆ {f} ({desc}) — {size:,} bytes\")\n+\n+            if not getattr(args, \"yes\", False):\n+                try:\n+                    answer = input(\"\\n  Type 'yes' to confirm: \").strip().lower()\n+                except (EOFError, KeyboardInterrupt):\n+                    print(\"\\n  Cancelled.\\n\")\n+                    return\n+                if answer != \"yes\":\n+                    print(\"  Cancelled.\\n\")\n+                    return\n+\n+            for f, desc in existing:\n+                (mem_dir / f).unlink()\n+                print(f\"  ✓ Deleted {f} ({desc})\")\n+\n+            print(f\"\\n  Memory reset complete. New sessions will start with a blank slate.\")\n+            print(f\"  Files were in: {display_hermes_home()}/memories/\\n\")\n         else:\n             from hermes_cli.memory_setup import memory_command\n             memory_command(args)"]},{"file":"tests/cli/test_cwd_env_respect.py","hunks":["@@ -0,0 +1,107 @@\n+\"\"\"Tests that load_cli_config() guards against lazy-import TERMINAL_CWD clobbering.\n+\n+When the gateway resolves TERMINAL_CWD at startup and cli.py is later\n+imported lazily (via delegate_tool → CLI_CONFIG), load_cli_config() must\n+not overwrite the already-resolved value with os.getcwd().\n+\n+config.yaml terminal.cwd is the canonical source of truth.\n+.env TERMINAL_CWD and MESSAGING_CWD are deprecated.\n+See issue #10817.\n+\"\"\"\n+\n+import os\n+import pytest\n+\n+\n+# The sentinel values that mean \"resolve at runtime\"\n+_CWD_PLACEHOLDERS = (\".\", \"auto\", \"cwd\")\n+\n+\n+def _resolve_terminal_cwd(terminal_config: dict, defaults: dict, env: dict):\n+    \"\"\"Simulate the CWD resolution logic from load_cli_config().\n+\n+    This mirrors the code in cli.py that checks for a pre-resolved\n+    TERMINAL_CWD before falling back to os.getcwd().\n+    \"\"\"\n+    if terminal_config.get(\"cwd\") in _CWD_PLACEHOLDERS:\n+        _existing_cwd = env.get(\"TERMINAL_CWD\", \"\")\n+        if _existing_cwd and _existing_cwd not in _CWD_PLACEHOLDERS and os.path.isabs(_existing_cwd):\n+            terminal_config[\"cwd\"] = _existing_cwd\n+            defaults[\"terminal\"][\"cwd\"] = _existing_cwd\n+        else:\n+            effective_backend = terminal_config.get(\"env_type\", \"local\")\n+            if effective_backend == \"local\":\n+                terminal_config[\"cwd\"] = \"/fake/getcwd\"  # stand-in for os.getcwd()\n+                defaults[\"terminal\"][\"cwd\"] = terminal_config[\"cwd\"]\n+            else:\n+                terminal_config.pop(\"cwd\", None)\n+\n+    # Simulate the bridging loop: write terminal_config[\"cwd\"] to env\n+    _file_has_terminal = defaults.get(\"_file_has_terminal\", False)\n+    if \"cwd\" in terminal_config:\n+        if _file_has_terminal or \"TERMINAL_CWD\" not in env:\n+            env[\"TERMINAL_CWD\"] = str(terminal_config[\"cwd\"])\n+\n+    return env.get(\"TERMINAL_CWD\", \"\")\n+\n+\n+class TestLazyImportGuard:\n+    \"\"\"TERMINAL_CWD resolved by gateway must survive a lazy cli.py import.\"\"\"\n+\n+    def test_gateway_resolved_cwd_survives(self):\n+        \"\"\"Gateway set TERMINAL_CWD → lazy cli import must not clobber.\"\"\"\n+        env = {\"TERMINAL_CWD\": \"/home/user/workspace\"}\n+        terminal_config = {\"cwd\": \".\", \"env_type\": \"local\"}\n+        defaults = {\"terminal\": {\"cwd\": \".\"}, \"_file_has_terminal\": False}\n+\n+        result = _resolve_terminal_cwd(terminal_config, defaults, env)\n+        assert result == \"/home/user/workspace\"\n+\n+    def test_gateway_resolved_cwd_survives_with_file_terminal(self):\n+        \"\"\"Even when config.yaml has a terminal: section, resolved CWD survives.\"\"\"\n+        env = {\"TERMINAL_CWD\": \"/home/user/workspace\"}\n+        terminal_config = {\"cwd\": \".\", \"env_type\": \"local\"}\n+        defaults = {\"terminal\": {\"cwd\": \".\"}, \"_file_has_terminal\": True}\n+\n+        result = _resolve_terminal_cwd(terminal_config, defaults, env)\n+        assert result == \"/home/user/workspace\"\n+\n+\n+class TestConfigCwdResolution:\n+    \"\"\"config.yaml terminal.cwd is the canonical source of truth.\"\"\"\n+\n+    def test_explicit_config_cwd_wins(self):\n+        \"\"\"terminal.cwd: /explicit/path always wins.\"\"\"\n+        env = {\"TERMINAL_CWD\": \"/old/gateway/value\"}\n+        terminal_config = {\"cwd\": \"/explicit/path\"}\n+        defaults = {\"terminal\": {\"cwd\": \"/explicit/path\"}, \"_file_has_terminal\": True}\n+\n+        result = _resolve_terminal_cwd(terminal_config, defaults, env)\n+        assert result == \"/explicit/path\"\n+\n+    def test_dot_cwd_resolves_to_getcwd_when_no_prior(self):\n+        \"\"\"With no pre-set TERMINAL_CWD, \".\" resolves to os.getcwd().\"\"\"\n+        env = {}\n+        terminal_config = {\"cwd\": \".\"}\n+        defaults = {\"terminal\": {\"cwd\": \".\"}, \"_file_has_terminal\": False}\n+\n+        result = _resolve_terminal_cwd(terminal_config, defaults, env)\n+        assert result == \"/fake/getcwd\"\n+\n+    def test_remote_backend_pops_cwd(self):\n+        \"\"\"Remote backend + placeholder cwd → popped for backend default.\"\"\"\n+        env = {}\n+        terminal_config = {\"cwd\": \".\", \"env_type\": \"docker\"}\n+        defaults = {\"terminal\": {\"cwd\": \".\"}, \"_file_has_terminal\": False}\n+\n+        result = _resolve_terminal_cwd(terminal_config, defaults, env)\n+        assert result == \"\"  # cwd popped, no env var set\n+\n+    def test_remote_backend_with_prior_cwd_preserves(self):\n+        \"\"\"Remote backend + pre-resolved TERMINAL_CWD → adopted.\"\"\"\n+        env = {\"TERMINAL_CWD\": \"/project\"}\n+        terminal_config = {\"cwd\": \".\", \"env_type\": \"docker\"}\n+        defaults = {\"terminal\": {\"cwd\": \".\"}, \"_file_has_terminal\": False}\n+\n+        result = _resolve_terminal_cwd(terminal_config, defaults, env)\n+        assert result == \"/project\""]},{"file":"tests/gateway/test_config_cwd_bridge.py","hunks":["@@ -37,6 +37,10 @@ def _simulate_config_bridge(cfg: dict, initial_env: dict | None = None):\n         for cfg_key, env_var in terminal_env_map.items():\n             if cfg_key in terminal_cfg:\n                 val = terminal_cfg[cfg_key]\n+                # Skip cwd placeholder values — don't overwrite already-resolved\n+                # TERMINAL_CWD.  Mirrors the fix in gateway/run.py.\n+                if cfg_key == \"cwd\" and str(val) in (\".\", \"auto\", \"cwd\"):\n+                    continue\n                 if isinstance(val, list):\n                     env[env_var] = json.dumps(val)\n                 else:","@@ -146,3 +150,58 @@ def test_top_level_cwd_beats_messaging_cwd(self):\n         cfg = {\"cwd\": \"/from/config\"}\n         result = _simulate_config_bridge(cfg, {\"MESSAGING_CWD\": \"/from/env\"})\n         assert result[\"TERMINAL_CWD\"] == \"/from/config\"\n+\n+\n+class TestNestedTerminalCwdPlaceholderSkip:\n+    \"\"\"terminal.cwd placeholder values must not clobber TERMINAL_CWD.\n+\n+    When config.yaml has terminal.cwd: \".\" (or \"auto\"/\"cwd\"), the gateway\n+    config bridge should NOT write that placeholder to TERMINAL_CWD.\n+    This prevents .env or MESSAGING_CWD values from being overwritten.\n+    See issues #10225, #4672, #10817.\n+    \"\"\"\n+\n+    def test_terminal_dot_cwd_does_not_clobber_env(self):\n+        \"\"\"terminal.cwd: '.' should not overwrite a pre-set TERMINAL_CWD.\"\"\"\n+        cfg = {\"terminal\": {\"cwd\": \".\"}}\n+        result = _simulate_config_bridge(cfg, {\"TERMINAL_CWD\": \"/my/project\"})\n+        assert result[\"TERMINAL_CWD\"] == \"/my/project\"\n+\n+    def test_terminal_auto_cwd_does_not_clobber_env(self):\n+        cfg = {\"terminal\": {\"cwd\": \"auto\"}}\n+        result = _simulate_config_bridge(cfg, {\"TERMINAL_CWD\": \"/my/project\"})\n+        assert result[\"TERMINAL_CWD\"] == \"/my/project\"\n+\n+    def test_terminal_cwd_keyword_does_not_clobber_env(self):\n+        cfg = {\"terminal\": {\"cwd\": \"cwd\"}}\n+        result = _simulate_config_bridge(cfg, {\"TERMINAL_CWD\": \"/my/project\"})\n+        assert result[\"TERMINAL_CWD\"] == \"/my/project\"\n+\n+    def test_terminal_explicit_cwd_does_override(self):\n+        \"\"\"terminal.cwd: '/explicit/path' SHOULD override TERMINAL_CWD.\"\"\"\n+        cfg = {\"terminal\": {\"cwd\": \"/explicit/path\"}}\n+        result = _simulate_config_bridge(cfg, {\"TERMINAL_CWD\": \"/old/value\"})\n+        assert result[\"TERMINAL_CWD\"] == \"/explicit/path\"\n+\n+    def test_terminal_dot_cwd_falls_back_to_messaging_cwd(self):\n+        \"\"\"terminal.cwd: '.' with no TERMINAL_CWD should fall to MESSAGING_CWD.\"\"\"\n+        cfg = {\"terminal\": {\"cwd\": \".\"}}\n+        result = _simulate_config_bridge(cfg, {\"MESSAGING_CWD\": \"/from/env\"})\n+        assert result[\"TERMINAL_CWD\"] == \"/from/env\"\n+\n+    def test_terminal_dot_cwd_and_messaging_cwd_both_set(self):\n+        \"\"\"Pre-set TERMINAL_CWD from .env wins over terminal.cwd: '.'.\"\"\"\n+        cfg = {\"terminal\": {\"cwd\": \".\", \"backend\": \"local\"}}\n+        result = _simulate_config_bridge(cfg, {\n+            \"TERMINAL_CWD\": \"/my/project\",\n+            \"MESSAGING_CWD\": \"/fallback\",\n+        })\n+        assert result[\"TERMINAL_CWD\"] == \"/my/project\"\n+\n+    def test_non_cwd_terminal_keys_still_bridge(self):\n+        \"\"\"Other terminal config keys (backend, timeout) should still bridge normally.\"\"\"\n+        cfg = {\"terminal\": {\"cwd\": \".\", \"backend\": \"docker\", \"timeout\": \"300\"}}\n+        result = _simulate_config_bridge(cfg, {\"MESSAGING_CWD\": \"/from/env\"})\n+        assert result[\"TERMINAL_ENV\"] == \"docker\"\n+        assert result[\"TERMINAL_TIMEOUT\"] == \"300\"\n+        assert result[\"TERMINAL_CWD\"] == \"/from/env\""]},{"file":"tests/hermes_cli/test_deprecated_cwd_warning.py","hunks":["@@ -0,0 +1,64 @@\n+\"\"\"Tests for warn_deprecated_cwd_env_vars() migration warning.\"\"\"\n+\n+import os\n+import pytest\n+\n+\n+class TestDeprecatedCwdWarning:\n+    \"\"\"Warn when MESSAGING_CWD or TERMINAL_CWD is set in .env.\"\"\"\n+\n+    def test_messaging_cwd_triggers_warning(self, monkeypatch, capsys):\n+        monkeypatch.setenv(\"MESSAGING_CWD\", \"/some/path\")\n+        monkeypatch.delenv(\"TERMINAL_CWD\", raising=False)\n+\n+        from hermes_cli.config import warn_deprecated_cwd_env_vars\n+        warn_deprecated_cwd_env_vars(config={})\n+\n+        captured = capsys.readouterr()\n+        assert \"MESSAGING_CWD\" in captured.err\n+        assert \"deprecated\" in captured.err.lower()\n+        assert \"config.yaml\" in captured.err\n+\n+    def test_terminal_cwd_triggers_warning_when_config_placeholder(self, monkeypatch, capsys):\n+        monkeypatch.setenv(\"TERMINAL_CWD\", \"/project\")\n+        monkeypatch.delenv(\"MESSAGING_CWD\", raising=False)\n+\n+        from hermes_cli.config import warn_deprecated_cwd_env_vars\n+        # config has placeholder cwd → TERMINAL_CWD likely from .env\n+        warn_deprecated_cwd_env_vars(config={\"terminal\": {\"cwd\": \".\"}})\n+\n+        captured = capsys.readouterr()\n+        assert \"TERMINAL_CWD\" in captured.err\n+        assert \"deprecated\" in captured.err.lower()\n+\n+    def test_no_warning_when_config_has_explicit_cwd(self, monkeypatch, capsys):\n+        monkeypatch.setenv(\"TERMINAL_CWD\", \"/project\")\n+        monkeypatch.delenv(\"MESSAGING_CWD\", raising=False)\n+\n+        from hermes_cli.config import warn_deprecated_cwd_env_vars\n+        # config has explicit cwd → TERMINAL_CWD could be from config bridge\n+        warn_deprecated_cwd_env_vars(config={\"terminal\": {\"cwd\": \"/project\"}})\n+\n+        captured = capsys.readouterr()\n+        assert \"TERMINAL_CWD\" not in captured.err\n+\n+    def test_no_warning_when_env_clean(self, monkeypatch, capsys):\n+        monkeypatch.delenv(\"MESSAGING_CWD\", raising=False)\n+        monkeypatch.delenv(\"TERMINAL_CWD\", raising=False)\n+\n+        from hermes_cli.config import warn_deprecated_cwd_env_vars\n+        warn_deprecated_cwd_env_vars(config={})\n+\n+        captured = capsys.readouterr()\n+        assert captured.err == \"\"\n+\n+    def test_both_deprecated_vars_warn(self, monkeypatch, capsys):\n+        monkeypatch.setenv(\"MESSAGING_CWD\", \"/msg/path\")\n+        monkeypatch.setenv(\"TERMINAL_CWD\", \"/term/path\")\n+\n+        from hermes_cli.config import warn_deprecated_cwd_env_vars\n+        warn_deprecated_cwd_env_vars(config={})\n+\n+        captured = capsys.readouterr()\n+        assert \"MESSAGING_CWD\" in captured.err\n+        assert \"TERMINAL_CWD\" in captured.err"]},{"file":"tests/hermes_cli/test_memory_reset.py","hunks":["@@ -0,0 +1,157 @@\n+\"\"\"Tests for the `hermes memory reset` CLI command.\n+\n+Covers:\n+- Reset both stores (MEMORY.md + USER.md)\n+- Reset individual stores (--target memory / --target user)\n+- Skip confirmation with --yes\n+- Graceful handling when no memory files exist\n+- Profile-scoped reset (uses HERMES_HOME)\n+\"\"\"\n+\n+import os\n+import pytest\n+from argparse import Namespace\n+from pathlib import Path\n+\n+\n[redacted:email]\n+def memory_env(tmp_path, monkeypatch):\n+    \"\"\"Set up a fake HERMES_HOME with memory files.\"\"\"\n+    hermes_home = tmp_path / \".hermes\"\n+    memories = hermes_home / \"memories\"\n+    memories.mkdir(parents=True)\n+    monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+\n+    # Create sample memory files\n+    (memories / \"MEMORY.md\").write_text(\n+        \"§\\nHermes repo is at ~/.hermes/hermes-agent\\n§\\nUser prefers dark themes\",\n+        encoding=\"utf-8\",\n+    )\n+    (memories / \"USER.md\").write_text(\n+        \"§\\nUser is Teknium\\n§\\nTimezone: US Pacific\",\n+        encoding=\"utf-8\",\n+    )\n+    return hermes_home, memories\n+\n+\n+def _run_memory_reset(target=\"all\", yes=False, monkeypatch=None, confirm_input=\"no\"):\n+    \"\"\"Invoke the memory reset logic from cmd_memory in main.py.\n+\n+    Simulates what happens when `hermes memory reset` is run.\n+    \"\"\"\n+    from hermes_constants import get_hermes_home, display_hermes_home\n+\n+    mem_dir = get_hermes_home() / \"memories\"\n+    files_to_reset = []\n+    if target in (\"all\", \"memory\"):\n+        files_to_reset.append((\"MEMORY.md\", \"agent notes\"))\n+    if target in (\"all\", \"user\"):\n+        files_to_reset.append((\"USER.md\", \"user profile\"))\n+\n+    existing = [(f, desc) for f, desc in files_to_reset if (mem_dir / f).exists()]\n+    if not existing:\n+        return \"nothing\"\n+\n+    if not yes:\n+        if confirm_input != \"yes\":\n+            return \"cancelled\"\n+\n+    for f, desc in existing:\n+        (mem_dir / f).unlink()\n+\n+    return \"deleted\"\n+\n+\n+class TestMemoryReset:\n+    \"\"\"Tests for `hermes memory reset` subcommand.\"\"\"\n+\n+    def test_reset_all_with_yes_flag(self, memory_env):\n+        \"\"\"--yes flag should skip confirmation and delete both files.\"\"\"\n+        hermes_home, memories = memory_env\n+        assert (memories / \"MEMORY.md\").exists()\n+        assert (memories / \"USER.md\").exists()\n+\n+        result = _run_memory_reset(target=\"all\", yes=True)\n+        assert result == \"deleted\"\n+        assert not (memories / \"MEMORY.md\").exists()\n+        assert not (memories / \"USER.md\").exists()\n+\n+    def test_reset_memory_only(self, memory_env):\n+        \"\"\"--target memory should only delete MEMORY.md.\"\"\"\n+        hermes_home, memories = memory_env\n+\n+        result = _run_memory_reset(target=\"memory\", yes=True)\n+        assert result == \"deleted\"\n+        assert not (memories / \"MEMORY.md\").exists()\n+        assert (memories / \"USER.md\").exists()\n+\n+    def test_reset_user_only(self, memory_env):\n+        \"\"\"--target user should only delete USER.md.\"\"\"\n+        hermes_home, memories = memory_env\n+\n+        result = _run_memory_reset(target=\"user\", yes=True)\n+        assert result == \"deleted\"\n+        assert (memories / \"MEMORY.md\").exists()\n+        assert not (memories / \"USER.md\").exists()\n+\n+    def test_reset_no_files_exist(self, tmp_path, monkeypatch):\n+        \"\"\"Should return 'nothing' when no memory files exist.\"\"\"\n+        hermes_home = tmp_path / \".hermes\"\n+        (hermes_home / \"memories\").mkdir(parents=True)\n+        monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+\n+        result = _run_memory_reset(target=\"all\", yes=True)\n+        assert result == \"nothing\"\n+\n+    def test_reset_confirmation_denied(self, memory_env):\n+        \"\"\"Without --yes and without typing 'yes', should be cancelled.\"\"\"\n+        hermes_home, memories = memory_env\n+\n+        result = _run_memory_reset(target=\"all\", yes=False, confirm_input=\"no\")\n+        assert result == \"cancelled\"\n+        # Files should still exist\n+        assert (memories / \"MEMORY.md\").exists()\n+        assert (memories / \"USER.md\").exists()\n+\n+    def test_reset_confirmation_accepted(self, memory_env):\n+        \"\"\"Typing 'yes' should proceed with deletion.\"\"\"\n+        hermes_home, memories = memory_env\n+\n+        result = _run_memory_reset(target=\"all\", yes=False, confirm_input=\"yes\")\n+        assert result == \"deleted\"\n+        assert not (memories / \"MEMORY.md\").exists()\n+        assert not (memories / \"USER.md\").exists()\n+\n+    def test_reset_profile_scoped(self, tmp_path, monkeypatch):\n+        \"\"\"Reset should work on the active profile's HERMES_HOME.\"\"\"\n+        profile_home = tmp_path / \"profiles\" / \"myprofile\"\n+        memories = profile_home / \"memories\"\n+        memories.mkdir(parents=True)\n+        (memories / \"MEMORY.md\").write_text(\"profile memory\", encoding=\"utf-8\")\n+        (memories / \"USER.md\").write_text(\"profile user\", encoding=\"utf-8\")\n+        monkeypatch.setenv(\"HERMES_HOME\", str(profile_home))\n+\n+        result = _run_memory_reset(target=\"all\", yes=True)\n+        assert result == \"deleted\"\n+        assert not (memories / \"MEMORY.md\").exists()\n+        assert not (memories / \"USER.md\").exists()\n+\n+    def test_reset_partial_files(self, memory_env):\n+        \"\"\"Reset should work when only one memory file exists.\"\"\"\n+        hermes_home, memories = memory_env\n+        (memories / \"USER.md\").unlink()\n+\n+        result = _run_memory_reset(target=\"all\", yes=True)\n+        assert result == \"deleted\"\n+        assert not (memories / \"MEMORY.md\").exists()\n+\n+    def test_reset_empty_memories_dir(self, tmp_path, monkeypatch):\n+        \"\"\"No memories dir at all should report nothing.\"\"\"\n+        hermes_home = tmp_path / \".hermes\"\n+        hermes_home.mkdir(parents=True)\n+        # No memories dir\n+        monkeypatch.setenv(\"HERMES_HOME\", str(hermes_home))\n+\n+        # The memories dir won't exist; get_hermes_home() / \"memories\" won't have files\n+        result = _run_memory_reset(target=\"all\", yes=True)\n+        assert result == \"nothing\"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}