diff --git a/headroom/learn/_shared.py b/headroom/learn/_shared.py index cb3a926f6..ba6f9944e 100644 --- a/headroom/learn/_shared.py +++ b/headroom/learn/_shared.py @@ -85,6 +85,14 @@ def classify_error(content: str) -> ErrorCategory: return ErrorCategory.UNKNOWN +# "exit code" only signals an error for a NONZERO code. Agent harnesses (Codex, +# Grok, opencode, ...) append "exit code 0" to every SUCCESSFUL shell command, +# so a bare "exit code" substring wrongly flagged those as errors and inflated +# the learned failure rate. Match a nonzero code (case-insensitive, so +# "Exit code: 1" counts too), never "exit code 0". +_NONZERO_EXIT_RE = re.compile(r"exit code:?\s*(?!0\b)\d", re.IGNORECASE) + + def is_error_content(content: str) -> bool: """Heuristic: does this tool result look like an error?""" if not content or len(content) < 10: @@ -105,10 +113,11 @@ def is_error_content(content: str) -> bool: "auto-denied", "Sibling tool call errored", "timed out", - "exit code", "FileNotFoundError", ] - return any(ind in snippet for ind in indicators) + if any(ind in snippet for ind in indicators): + return True + return bool(_NONZERO_EXIT_RE.search(snippet)) # ============================================================================= diff --git a/tests/test_learn/test_integration.py b/tests/test_learn/test_integration.py index c1b21215c..3ed01d189 100644 --- a/tests/test_learn/test_integration.py +++ b/tests/test_learn/test_integration.py @@ -114,6 +114,20 @@ class TestFalsePositiveFiltering: ) assert is_error_content("bash: unknown_cmd: command not found") + def test_exit_code_zero_is_not_an_error(self): + """Agent harnesses append 'exit code 0' to every successful command; + that must not be classified as an error.""" + from headroom.learn.scanner import is_error_content + + assert not is_error_content("Ran the tests.\nProcess finished with exit code 0") + + def test_nonzero_exit_code_is_an_error(self): + """A nonzero exit code (any casing / with a colon) is still an error.""" + from headroom.learn.scanner import is_error_content + + assert is_error_content("build failed\ncommand exited with exit code 1") + assert is_error_content("npm run build\nExit code: 127") + # ============================================================================= # Real-World Integration Tests (skipped if data not present)