From bc7a5bf4bbd5773e2110035e73beafcc11d0f14a Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Tue, 17 Mar 2026 18:48:17 +0000 Subject: [PATCH] fix: output structured XML errors in subagent mode When codeflash runs with --subagent (e.g., via the Claude Code plugin), exit_with_message() now outputs XML to stdout instead of Rich panel text. This lets the calling agent parse errors programmatically rather than receiving unstructured text. Co-Authored-By: Claude Opus 4.6 --- codeflash/code_utils/code_utils.py | 7 ++++++- tests/test_code_utils.py | 31 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/codeflash/code_utils/code_utils.py b/codeflash/code_utils/code_utils.py index 45a64f0fc..20b2beaa2 100644 --- a/codeflash/code_utils/code_utils.py +++ b/codeflash/code_utils/code_utils.py @@ -17,7 +17,7 @@ from codeflash.cli_cmds.console import logger, paneled_text from codeflash.code_utils.config_parser import find_pyproject_toml, get_all_closest_config_files -from codeflash.lsp.helpers import is_LSP_enabled +from codeflash.lsp.helpers import is_LSP_enabled, is_subagent_mode _INVALID_CHARS_NT = {"<", ">", ":", '"', "|", "?", "*"} @@ -458,6 +458,11 @@ def exit_with_message(message: str, *, error_on_exit: bool = False) -> None: if is_LSP_enabled(): logger.error(message) return + if is_subagent_mode(): + from xml.sax.saxutils import escape + + sys.stdout.write(f"{escape(message)}\n") + sys.exit(1 if error_on_exit else 0) paneled_text(message, panel_args={"style": "red"}) sys.exit(1 if error_on_exit else 0) diff --git a/tests/test_code_utils.py b/tests/test_code_utils.py index 1d792685b..976120bbe 100644 --- a/tests/test_code_utils.py +++ b/tests/test_code_utils.py @@ -8,6 +8,7 @@ from codeflash.code_utils.code_utils import ( cleanup_paths, + exit_with_message, file_name_from_test_module_name, file_path_from_module_name, get_all_function_names, @@ -751,3 +752,33 @@ def __init__(self): """ result = validate_python_code(code) assert result == code + + +class TestExitWithMessageSubagent: + @patch("codeflash.code_utils.code_utils.is_subagent_mode", return_value=True) + def test_outputs_structured_xml_in_subagent_mode(self, _mock_subagent: MagicMock, capsys: pytest.CaptureFixture[str]) -> None: + with pytest.raises(SystemExit) as exc_info: + exit_with_message("Something went wrong", error_on_exit=True) + assert exc_info.value.code == 1 + captured = capsys.readouterr() + assert "" in captured.out + assert "Something went wrong" in captured.out + assert "" in captured.out + + @patch("codeflash.code_utils.code_utils.is_subagent_mode", return_value=True) + def test_escapes_xml_special_chars(self, _mock_subagent: MagicMock, capsys: pytest.CaptureFixture[str]) -> None: + with pytest.raises(SystemExit): + exit_with_message('File & "bar" not found', error_on_exit=True) + captured = capsys.readouterr() + assert "<foo>" in captured.out + assert "&" in captured.out + + @patch("codeflash.code_utils.code_utils.is_subagent_mode", return_value=False) + @patch("codeflash.code_utils.code_utils.is_LSP_enabled", return_value=False) + def test_no_xml_when_not_subagent( + self, _mock_lsp: MagicMock, _mock_subagent: MagicMock, capsys: pytest.CaptureFixture[str] + ) -> None: + with pytest.raises(SystemExit): + exit_with_message("Normal error", error_on_exit=True) + captured = capsys.readouterr() + assert "" not in captured.out