From e18a4b7ca5e4888c855e214a23da74ac864d65e3 Mon Sep 17 00:00:00 2001 From: Bill Kramme Date: Mon, 16 Mar 2026 08:14:29 -0500 Subject: [PATCH 1/9] fix: /cd command now reloads agent context including AGENTS.md Fixes issue where /cd command would change the working directory but the agent context would not be refreshed, causing: - AGENTS.md files in new directory to be ignored - Stale working directory in system prompts - Plugin callbacks not re-firing with new context Changes: - Modified /cd command to clear _puppy_rules cache and reload agent - Added agent reload with graceful error handling - Added two comprehensive tests to verify reload behavior This ensures that when users change directories, the agent picks up project-specific AGENTS.md rules and refreshes all context-dependent prompt components. --- code_puppy/command_line/core_commands.py | 24 ++++---- .../test_core_commands_extended.py | 56 +++++++++++++++++++ 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/code_puppy/command_line/core_commands.py b/code_puppy/command_line/core_commands.py index 0608baf89..19e9ec95a 100644 --- a/code_puppy/command_line/core_commands.py +++ b/code_puppy/command_line/core_commands.py @@ -77,20 +77,22 @@ def handle_cd_command(command: str) -> bool: if os.path.isdir(target): os.chdir(target) emit_success(f"Changed directory to: {target}") - # Reload the agent so the system prompt and project-local - # AGENT.md rules reflect the new working directory. Without - # this, the LLM keeps receiving stale path information for the - # remainder of the session (the PydanticAgent instructions are - # baked in at construction time and never refreshed otherwise). + + # Reload the agent to pick up new working directory context + # This ensures AGENTS.md is re-read and system prompt is updated try: from code_puppy.agents.agent_manager import get_current_agent - - get_current_agent().reload_code_generation_agent() + + current_agent = get_current_agent() + if current_agent: + # Clear cached puppy rules so AGENTS.md is re-read from new directory + current_agent._puppy_rules = None + # Reload agent to rebuild system prompt with new working directory + current_agent.reload_code_generation_agent() + emit_info("Agent context updated for new directory") except Exception as e: - emit_warning( - f"Directory changed, but agent reload failed: {e}. " - "You may need to run /agent or /model to force a refresh." - ) + # Non-fatal: directory change succeeded even if reload failed + emit_error(f"Warning: Could not reload agent context: {e}") else: emit_error(f"Not a directory: {dirname}") return True diff --git a/tests/command_line/test_core_commands_extended.py b/tests/command_line/test_core_commands_extended.py index f16e097f3..008d9325c 100644 --- a/tests/command_line/test_core_commands_extended.py +++ b/tests/command_line/test_core_commands_extended.py @@ -134,6 +134,62 @@ def test_cd_listing_with_permission_error(self): args, kwargs = mock_error.call_args assert "Access denied" in args[0] + def test_cd_reloads_agent_context(self): + """Test that /cd reloads agent to pick up new directory context (AGENTS.md, etc).""" + from unittest.mock import MagicMock + + mock_agent = MagicMock() + mock_agent._puppy_rules = "old cached rules" + + with patch("code_puppy.messaging.emit_success"): + with patch("code_puppy.messaging.emit_info"): + with patch("os.path.expanduser", side_effect=lambda x: x): + with patch("os.path.isabs", return_value=True): + with patch("os.path.isdir", return_value=True): + with patch("os.chdir"): + with patch( + "code_puppy.agents.agent_manager.get_current_agent", + return_value=mock_agent + ): + result = handle_cd_command("/cd /new/dir") + + # Verify directory changed + assert result is True + + # Verify agent cache was cleared + assert mock_agent._puppy_rules is None + + # Verify agent was reloaded + mock_agent.reload_code_generation_agent.assert_called_once() + + def test_cd_handles_agent_reload_failure_gracefully(self): + """Test that /cd continues even if agent reload fails.""" + from unittest.mock import MagicMock + + mock_agent = MagicMock() + mock_agent.reload_code_generation_agent.side_effect = Exception("Reload failed") + + with patch("code_puppy.messaging.emit_success"): + with patch("code_puppy.messaging.emit_error") as mock_error: + with patch("os.path.expanduser", side_effect=lambda x: x): + with patch("os.path.isabs", return_value=True): + with patch("os.path.isdir", return_value=True): + with patch("os.chdir"): + with patch( + "code_puppy.agents.agent_manager.get_current_agent", + return_value=mock_agent + ): + result = handle_cd_command("/cd /new/dir") + + # Directory change should still succeed + assert result is True + + # Error should be emitted about reload failure + assert mock_error.called + error_msg = mock_error.call_args[0][0] + assert "Warning" in error_msg + assert "Could not reload agent context" in error_msg + def test_cd_with_nonexistent_parent(self): """Test cd command with path containing nonexistent parent directories.""" with patch("code_puppy.messaging.emit_error") as mock_error: From 332d9eae9dd4208e87e203454084089fd39354fb Mon Sep 17 00:00:00 2001 From: Bill Kramme Date: Mon, 16 Mar 2026 08:19:54 -0500 Subject: [PATCH 2/9] style: remove unused emit_warning import --- code_puppy/command_line/core_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_puppy/command_line/core_commands.py b/code_puppy/command_line/core_commands.py index 19e9ec95a..278d9f1d1 100644 --- a/code_puppy/command_line/core_commands.py +++ b/code_puppy/command_line/core_commands.py @@ -55,7 +55,7 @@ def handle_cd_command(command: str) -> bool: # Use shlex.split to handle quoted paths properly import shlex - from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning + from code_puppy.messaging import emit_error, emit_info, emit_success try: tokens = shlex.split(command) From 35a6a650f4eb54d723a71ab17a86c1332207ec12 Mon Sep 17 00:00:00 2001 From: Bill Kramme Date: Mon, 16 Mar 2026 08:23:42 -0500 Subject: [PATCH 3/9] refactor: remove direct _puppy_rules mutation from command layer - Removed direct mutation of agent._puppy_rules from /cd command - reload_code_generation_agent() now handles invalidation internally - Updated test to verify public behavior only (reload called) - Removed brittle assertion on private _puppy_rules field This reduces coupling and makes the code more maintainable. --- code_puppy/command_line/core_commands.py | 5 ++--- tests/command_line/test_core_commands_extended.py | 6 +----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/code_puppy/command_line/core_commands.py b/code_puppy/command_line/core_commands.py index 278d9f1d1..75a5065c6 100644 --- a/code_puppy/command_line/core_commands.py +++ b/code_puppy/command_line/core_commands.py @@ -85,9 +85,8 @@ def handle_cd_command(command: str) -> bool: current_agent = get_current_agent() if current_agent: - # Clear cached puppy rules so AGENTS.md is re-read from new directory - current_agent._puppy_rules = None - # Reload agent to rebuild system prompt with new working directory + # reload_code_generation_agent() invalidates cached rules + # and rebuilds prompt/context from the new cwd current_agent.reload_code_generation_agent() emit_info("Agent context updated for new directory") except Exception as e: diff --git a/tests/command_line/test_core_commands_extended.py b/tests/command_line/test_core_commands_extended.py index 008d9325c..b956d567e 100644 --- a/tests/command_line/test_core_commands_extended.py +++ b/tests/command_line/test_core_commands_extended.py @@ -139,7 +139,6 @@ def test_cd_reloads_agent_context(self): from unittest.mock import MagicMock mock_agent = MagicMock() - mock_agent._puppy_rules = "old cached rules" with patch("code_puppy.messaging.emit_success"): with patch("code_puppy.messaging.emit_info"): @@ -156,10 +155,7 @@ def test_cd_reloads_agent_context(self): # Verify directory changed assert result is True - # Verify agent cache was cleared - assert mock_agent._puppy_rules is None - - # Verify agent was reloaded + # Verify agent was reloaded (public behavior) mock_agent.reload_code_generation_agent.assert_called_once() def test_cd_handles_agent_reload_failure_gracefully(self): From 4e94d1dbb526ca390314b8060f676bb81c20bd8c Mon Sep 17 00:00:00 2001 From: Bill Kramme Date: Mon, 16 Mar 2026 08:30:21 -0500 Subject: [PATCH 4/9] style: remove redundant MagicMock imports in tests MagicMock is already imported at the top of the test file. Removed local re-imports in test_cd_reloads_agent_context and test_cd_handles_agent_reload_failure_gracefully. --- tests/command_line/test_core_commands_extended.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/command_line/test_core_commands_extended.py b/tests/command_line/test_core_commands_extended.py index b956d567e..711878b0f 100644 --- a/tests/command_line/test_core_commands_extended.py +++ b/tests/command_line/test_core_commands_extended.py @@ -136,8 +136,6 @@ def test_cd_listing_with_permission_error(self): def test_cd_reloads_agent_context(self): """Test that /cd reloads agent to pick up new directory context (AGENTS.md, etc).""" - from unittest.mock import MagicMock - mock_agent = MagicMock() with patch("code_puppy.messaging.emit_success"): @@ -160,8 +158,6 @@ def test_cd_reloads_agent_context(self): def test_cd_handles_agent_reload_failure_gracefully(self): """Test that /cd continues even if agent reload fails.""" - from unittest.mock import MagicMock - mock_agent = MagicMock() mock_agent.reload_code_generation_agent.side_effect = Exception("Reload failed") From 2c1e37a1e06b09912f0701352e62297e739e51e7 Mon Sep 17 00:00:00 2001 From: Bill Kramme Date: Mon, 16 Mar 2026 09:00:01 -0500 Subject: [PATCH 5/9] fix: update test to expect emit_error instead of emit_warning The /cd command now uses emit_error for reload failures (not emit_warning). Also applied ruff formatting for consistency. --- code_puppy/command_line/core_commands.py | 4 ++-- .../test_core_commands_extended.py | 18 +++++++++--------- tests/test_command_handler.py | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/code_puppy/command_line/core_commands.py b/code_puppy/command_line/core_commands.py index 75a5065c6..80e08a9c3 100644 --- a/code_puppy/command_line/core_commands.py +++ b/code_puppy/command_line/core_commands.py @@ -77,12 +77,12 @@ def handle_cd_command(command: str) -> bool: if os.path.isdir(target): os.chdir(target) emit_success(f"Changed directory to: {target}") - + # Reload the agent to pick up new working directory context # This ensures AGENTS.md is re-read and system prompt is updated try: from code_puppy.agents.agent_manager import get_current_agent - + current_agent = get_current_agent() if current_agent: # reload_code_generation_agent() invalidates cached rules diff --git a/tests/command_line/test_core_commands_extended.py b/tests/command_line/test_core_commands_extended.py index 711878b0f..a750c9b67 100644 --- a/tests/command_line/test_core_commands_extended.py +++ b/tests/command_line/test_core_commands_extended.py @@ -137,7 +137,7 @@ def test_cd_listing_with_permission_error(self): def test_cd_reloads_agent_context(self): """Test that /cd reloads agent to pick up new directory context (AGENTS.md, etc).""" mock_agent = MagicMock() - + with patch("code_puppy.messaging.emit_success"): with patch("code_puppy.messaging.emit_info"): with patch("os.path.expanduser", side_effect=lambda x: x): @@ -146,21 +146,21 @@ def test_cd_reloads_agent_context(self): with patch("os.chdir"): with patch( "code_puppy.agents.agent_manager.get_current_agent", - return_value=mock_agent + return_value=mock_agent, ): result = handle_cd_command("/cd /new/dir") - + # Verify directory changed assert result is True - + # Verify agent was reloaded (public behavior) mock_agent.reload_code_generation_agent.assert_called_once() - + def test_cd_handles_agent_reload_failure_gracefully(self): """Test that /cd continues even if agent reload fails.""" mock_agent = MagicMock() mock_agent.reload_code_generation_agent.side_effect = Exception("Reload failed") - + with patch("code_puppy.messaging.emit_success"): with patch("code_puppy.messaging.emit_error") as mock_error: with patch("os.path.expanduser", side_effect=lambda x: x): @@ -169,13 +169,13 @@ def test_cd_handles_agent_reload_failure_gracefully(self): with patch("os.chdir"): with patch( "code_puppy.agents.agent_manager.get_current_agent", - return_value=mock_agent + return_value=mock_agent, ): result = handle_cd_command("/cd /new/dir") - + # Directory change should still succeed assert result is True - + # Error should be emitted about reload failure assert mock_error.called error_msg = mock_error.call_args[0][0] diff --git a/tests/test_command_handler.py b/tests/test_command_handler.py index ed7459daa..1d88ed2ef 100644 --- a/tests/test_command_handler.py +++ b/tests/test_command_handler.py @@ -90,7 +90,7 @@ def test_cd_valid_change_reload_failure_is_nonfatal(): """A reload failure after /cd must not abort the directory change.""" mocks = setup_messaging_mocks() mock_emit_success = mocks["emit_success"].start() - mock_emit_warning = mocks["emit_warning"].start() + mock_emit_error = mocks["emit_error"].start() try: mock_agent = MagicMock() @@ -111,11 +111,11 @@ def test_cd_valid_change_reload_failure_is_nonfatal(): mock_chdir.assert_called_once_with("/some/dir") mock_emit_success.assert_called_once_with("Changed directory to: /some/dir") mock_agent.reload_code_generation_agent.assert_called_once() - # Reload failure should emit a warning, not silently pass - mock_emit_warning.assert_called_once() - warning_msg = str(mock_emit_warning.call_args) - assert "agent reload failed" in warning_msg - assert "boom" in warning_msg + # Reload failure should emit an error, not silently pass + mock_emit_error.assert_called_once() + error_msg = str(mock_emit_error.call_args) + assert "agent reload failed" in error_msg or "Could not reload agent context" in error_msg + assert "boom" in error_msg finally: mocks["emit_success"].stop() mocks["emit_warning"].stop() From cc6c7fc5b6f6cc14e6c77f45334eb3ae1112e350 Mon Sep 17 00:00:00 2001 From: Bill Kramme Date: Mon, 16 Mar 2026 09:07:59 -0500 Subject: [PATCH 6/9] fix: address CodeRabbit review feedback - Fix mock cleanup leak (stop emit_error instead of emit_warning) - Remove 'Warning:' prefix from emit_error message for consistency - Add assertion for emit_info call in test_cd_reloads_agent_context - Update test to expect new error message format (no 'Warning:' prefix) --- code_puppy/command_line/core_commands.py | 2 +- tests/command_line/test_core_commands_extended.py | 8 ++++++-- tests/test_command_handler.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/code_puppy/command_line/core_commands.py b/code_puppy/command_line/core_commands.py index 80e08a9c3..df3932a7b 100644 --- a/code_puppy/command_line/core_commands.py +++ b/code_puppy/command_line/core_commands.py @@ -91,7 +91,7 @@ def handle_cd_command(command: str) -> bool: emit_info("Agent context updated for new directory") except Exception as e: # Non-fatal: directory change succeeded even if reload failed - emit_error(f"Warning: Could not reload agent context: {e}") + emit_error(f"Could not reload agent context: {e}") else: emit_error(f"Not a directory: {dirname}") return True diff --git a/tests/command_line/test_core_commands_extended.py b/tests/command_line/test_core_commands_extended.py index a750c9b67..426ad3769 100644 --- a/tests/command_line/test_core_commands_extended.py +++ b/tests/command_line/test_core_commands_extended.py @@ -139,7 +139,7 @@ def test_cd_reloads_agent_context(self): mock_agent = MagicMock() with patch("code_puppy.messaging.emit_success"): - with patch("code_puppy.messaging.emit_info"): + with patch("code_puppy.messaging.emit_info") as mock_emit_info: with patch("os.path.expanduser", side_effect=lambda x: x): with patch("os.path.isabs", return_value=True): with patch("os.path.isdir", return_value=True): @@ -156,6 +156,11 @@ def test_cd_reloads_agent_context(self): # Verify agent was reloaded (public behavior) mock_agent.reload_code_generation_agent.assert_called_once() + # Verify context refresh message was emitted + mock_emit_info.assert_called_once_with( + "Agent context updated for new directory" + ) + def test_cd_handles_agent_reload_failure_gracefully(self): """Test that /cd continues even if agent reload fails.""" mock_agent = MagicMock() @@ -179,7 +184,6 @@ def test_cd_handles_agent_reload_failure_gracefully(self): # Error should be emitted about reload failure assert mock_error.called error_msg = mock_error.call_args[0][0] - assert "Warning" in error_msg assert "Could not reload agent context" in error_msg def test_cd_with_nonexistent_parent(self): diff --git a/tests/test_command_handler.py b/tests/test_command_handler.py index 1d88ed2ef..01030b846 100644 --- a/tests/test_command_handler.py +++ b/tests/test_command_handler.py @@ -118,7 +118,7 @@ def test_cd_valid_change_reload_failure_is_nonfatal(): assert "boom" in error_msg finally: mocks["emit_success"].stop() - mocks["emit_warning"].stop() + mocks["emit_error"].stop() def test_cd_invalid_directory(): From 8af48733e85011fe59c67b0e3329dbaac541622f Mon Sep 17 00:00:00 2001 From: Bill Kramme Date: Mon, 16 Mar 2026 09:17:49 -0500 Subject: [PATCH 7/9] style: apply ruff formatting to test files Applied black-compatible formatting to 3 test files for consistency. --- tests/agents/test_base_agent_full_coverage.py | 8 +++++--- .../command_line/test_model_settings_menu_coverage.py | 11 +++++++---- tests/test_command_handler.py | 5 ++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/tests/agents/test_base_agent_full_coverage.py b/tests/agents/test_base_agent_full_coverage.py index 4165df12e..e33cfa07e 100644 --- a/tests/agents/test_base_agent_full_coverage.py +++ b/tests/agents/test_base_agent_full_coverage.py @@ -2129,9 +2129,11 @@ def test_loads_from_project_dir(self, agent, tmp_path): patch("code_puppy.config.CONFIG_DIR", str(tmp_path / "nonexistent")), patch( "pathlib.Path.exists", - side_effect=lambda self: str(self) == str(rules_file) - or str(self).endswith("AGENTS.md") - and "nonexistent" not in str(self), + side_effect=lambda self: ( + str(self) == str(rules_file) + or str(self).endswith("AGENTS.md") + and "nonexistent" not in str(self) + ), ), ): # Complex to test due to pathlib patching, just test cached path diff --git a/tests/command_line/test_model_settings_menu_coverage.py b/tests/command_line/test_model_settings_menu_coverage.py index 97e257f24..c96a224c8 100644 --- a/tests/command_line/test_model_settings_menu_coverage.py +++ b/tests/command_line/test_model_settings_menu_coverage.py @@ -151,10 +151,13 @@ def test_get_supported_settings(self, mock_supports): def test_load_model_settings_with_openai( self, mock_supports, mock_get_all, mock_effort, mock_verb ): - mock_supports.side_effect = lambda m, s: s in ( - "temperature", - "reasoning_effort", - "verbosity", + mock_supports.side_effect = lambda m, s: ( + s + in ( + "temperature", + "reasoning_effort", + "verbosity", + ) ) menu = _make_menu() menu._load_model_settings("gpt-5") diff --git a/tests/test_command_handler.py b/tests/test_command_handler.py index 01030b846..80da4ccdc 100644 --- a/tests/test_command_handler.py +++ b/tests/test_command_handler.py @@ -114,7 +114,10 @@ def test_cd_valid_change_reload_failure_is_nonfatal(): # Reload failure should emit an error, not silently pass mock_emit_error.assert_called_once() error_msg = str(mock_emit_error.call_args) - assert "agent reload failed" in error_msg or "Could not reload agent context" in error_msg + assert ( + "agent reload failed" in error_msg + or "Could not reload agent context" in error_msg + ) assert "boom" in error_msg finally: mocks["emit_success"].stop() From 9d21feeedd45ed5a5dfad2b86b147ba21cd49350 Mon Sep 17 00:00:00 2001 From: Bill Kramme Date: Mon, 16 Mar 2026 09:24:41 -0500 Subject: [PATCH 8/9] test: tighten error assertion robustness Per CodeRabbit review: - Use assert_called_once() instead of assert called - Check exact prefix with startswith() - Assert original exception text is present - Use call_args[0][0] instead of str(call_args) - Remove redundant 'agent reload failed' dead code check --- tests/command_line/test_core_commands_extended.py | 7 +++++-- tests/test_command_handler.py | 7 ++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/command_line/test_core_commands_extended.py b/tests/command_line/test_core_commands_extended.py index 426ad3769..f3b1166f5 100644 --- a/tests/command_line/test_core_commands_extended.py +++ b/tests/command_line/test_core_commands_extended.py @@ -182,9 +182,12 @@ def test_cd_handles_agent_reload_failure_gracefully(self): assert result is True # Error should be emitted about reload failure - assert mock_error.called + mock_error.assert_called_once() error_msg = mock_error.call_args[0][0] - assert "Could not reload agent context" in error_msg + assert error_msg.startswith( + "Could not reload agent context:" + ) + assert "Reload failed" in error_msg def test_cd_with_nonexistent_parent(self): """Test cd command with path containing nonexistent parent directories.""" diff --git a/tests/test_command_handler.py b/tests/test_command_handler.py index 80da4ccdc..dd40715d4 100644 --- a/tests/test_command_handler.py +++ b/tests/test_command_handler.py @@ -113,11 +113,8 @@ def test_cd_valid_change_reload_failure_is_nonfatal(): mock_agent.reload_code_generation_agent.assert_called_once() # Reload failure should emit an error, not silently pass mock_emit_error.assert_called_once() - error_msg = str(mock_emit_error.call_args) - assert ( - "agent reload failed" in error_msg - or "Could not reload agent context" in error_msg - ) + error_msg = mock_emit_error.call_args[0][0] + assert error_msg.startswith("Could not reload agent context:") assert "boom" in error_msg finally: mocks["emit_success"].stop() From 3ef2dbe414d3011ac29bfba326a12ce959be4772 Mon Sep 17 00:00:00 2001 From: Bill Kramme Date: Tue, 14 Apr 2026 08:35:29 -0500 Subject: [PATCH 9/9] refactor(cd): remove redundant current_agent guard --- code_puppy/command_line/core_commands.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/code_puppy/command_line/core_commands.py b/code_puppy/command_line/core_commands.py index df3932a7b..0927c99cb 100644 --- a/code_puppy/command_line/core_commands.py +++ b/code_puppy/command_line/core_commands.py @@ -83,12 +83,10 @@ def handle_cd_command(command: str) -> bool: try: from code_puppy.agents.agent_manager import get_current_agent - current_agent = get_current_agent() - if current_agent: - # reload_code_generation_agent() invalidates cached rules - # and rebuilds prompt/context from the new cwd - current_agent.reload_code_generation_agent() - emit_info("Agent context updated for new directory") + # reload_code_generation_agent() invalidates cached rules + # and rebuilds prompt/context from the new cwd + get_current_agent().reload_code_generation_agent() + emit_info("Agent context updated for new directory") except Exception as e: # Non-fatal: directory change succeeded even if reload failed emit_error(f"Could not reload agent context: {e}")