diff --git a/ymir/agents/triage_agent.py b/ymir/agents/triage_agent.py index aede715b7..994e6aafa 100644 --- a/ymir/agents/triage_agent.py +++ b/ymir/agents/triage_agent.py @@ -593,13 +593,39 @@ async def check_cve_eligibility(state): f"Issue {state.jira_issue} not eligible for triage: {state.cve_eligibility_result.reason}" ) if state.cve_eligibility_result.error: - state.triage_result = OutputSchema( - resolution=Resolution.ERROR, - data=ErrorData( - details=f"CVE eligibility check error: {state.cve_eligibility_result.error}", - jira_issue=state.jira_issue, - ), - ) + # Distinguish human-correctable errors from transient/operational failures: + # - Missing Fix Versions field → CLARIFICATION_NEEDED (non-retriable, + # maps to ymir_needs_attention) + # - Clone/dependency check failures → ERROR (retriable, + # maps to ymir_triage_errored) + error_msg = state.cve_eligibility_result.error + is_missing_fix_version = "Fix Versions field is empty" in error_msg + + if is_missing_fix_version: + # Non-retriable: requires human intervention (e.g., setting Fix Versions field) + # Use CLARIFICATION_NEEDED to skip retry loop and post comment only once + # Maps to ymir_needs_attention label (issue data incomplete, needs human attention) + state.triage_result = OutputSchema( + resolution=Resolution.CLARIFICATION_NEEDED, + data=ClarificationNeededData( + findings=f"CVE eligibility check error: {error_msg}", + additional_info_needed=( + "Please fix the issue and retry manually (e.g., via ymir_todo label)." + ), + jira_issue=state.jira_issue, + ), + ) + else: + # Retriable: operational/transient errors (network, API failures, etc.) + # Use ERROR resolution to trigger retry logic + # Maps to ymir_triage_errored label after max retries exhausted + state.triage_result = OutputSchema( + resolution=Resolution.ERROR, + data=ErrorData( + details=f"CVE eligibility check error: {error_msg}", + jira_issue=state.jira_issue, + ), + ) elif dup_key: state.triage_result = OutputSchema( resolution=Resolution.OPEN_ENDED_ANALYSIS, diff --git a/ymir/tools/privileged/jira.py b/ymir/tools/privileged/jira.py index 716a76366..ebe01a29d 100644 --- a/ymir/tools/privileged/jira.py +++ b/ymir/tools/privileged/jira.py @@ -808,8 +808,8 @@ async def _run( CVEEligibilityResult( is_cve=True, eligibility=TriageEligibility.NEVER, - reason="CVE has no target release specified", - error="CVE has no target release specified", + reason="CVE has no target release specified (Fix Versions field is empty)", + error="CVE has no target release specified (Fix Versions field is empty)", ).model_dump() ) diff --git a/ymir/tools/privileged/tests/unit/test_jira.py b/ymir/tools/privileged/tests/unit/test_jira.py index 1b7167e45..3264cde61 100644 --- a/ymir/tools/privileged/tests/unit/test_jira.py +++ b/ymir/tools/privileged/tests/unit/test_jira.py @@ -1105,6 +1105,9 @@ async def test_eligibility_no_fix_version(): result = (await CheckCveTriageEligibilityTool().run(input={"issue_key": "RHEL-12345"})).result assert result["eligibility"] == TriageEligibility.NEVER assert result["error"] is not None + # Verify the clarified error message that specifies which Jira field is missing + assert "Fix Versions field is empty" in result["error"] + assert "Fix Versions field is empty" in result["reason"] @pytest.mark.asyncio