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 <codeflash-error> 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 <noreply@anthropic.com>
This commit is contained in:
Mohamed Ashraf 2026-03-17 18:48:17 +00:00
parent 1e92f3d2ed
commit bc7a5bf4bb
2 changed files with 37 additions and 1 deletions

View file

@ -17,7 +17,7 @@ import tomlkit
from codeflash.cli_cmds.console import logger, paneled_text 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.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 = {"<", ">", ":", '"', "|", "?", "*"} _INVALID_CHARS_NT = {"<", ">", ":", '"', "|", "?", "*"}
@ -458,6 +458,11 @@ def exit_with_message(message: str, *, error_on_exit: bool = False) -> None:
if is_LSP_enabled(): if is_LSP_enabled():
logger.error(message) logger.error(message)
return return
if is_subagent_mode():
from xml.sax.saxutils import escape
sys.stdout.write(f"<codeflash-error>{escape(message)}</codeflash-error>\n")
sys.exit(1 if error_on_exit else 0)
paneled_text(message, panel_args={"style": "red"}) paneled_text(message, panel_args={"style": "red"})
sys.exit(1 if error_on_exit else 0) sys.exit(1 if error_on_exit else 0)

View file

@ -8,6 +8,7 @@ import pytest
from codeflash.code_utils.code_utils import ( from codeflash.code_utils.code_utils import (
cleanup_paths, cleanup_paths,
exit_with_message,
file_name_from_test_module_name, file_name_from_test_module_name,
file_path_from_module_name, file_path_from_module_name,
get_all_function_names, get_all_function_names,
@ -751,3 +752,33 @@ class MyClass:
""" """
result = validate_python_code(code) result = validate_python_code(code)
assert result == 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 "<codeflash-error>" in captured.out
assert "Something went wrong" in captured.out
assert "</codeflash-error>" 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 <foo> & "bar" not found', error_on_exit=True)
captured = capsys.readouterr()
assert "&lt;foo&gt;" in captured.out
assert "&amp;" 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 "<codeflash-error>" not in captured.out