fix(hermes): stop describing a finding as a failed build
All checks were successful
Tests / Declarative: Post Actions passed: 1440

The first advice issues went out rendered through build-failure wording and
were wrong in four places at once: the title printed a source line as a build
number, the header claimed a first failed gate for a build that passed, the
heading asked why a human was needed when nothing had broken, and the
SonarQube link was labelled "Failed build".

Every one of those is small and every one of them is the kind of thing that
makes a reader distrust the rest of the page - which matters more here than
usual, because the whole point of the issue is a suggestion they have to judge
for themselves.

A finding-sourced issue now says what it is: SonarQube reports this rule in
this file, the build is green, this is a standing finding rather than a
failure. Build-failure issues are untouched, and a test asserts each wording
stays out of the other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
codex 2026-08-07 05:22:03 -03:00
parent 03f5fbf599
commit d876c7a6fb
3 changed files with 88 additions and 7 deletions

View File

@ -22,6 +22,17 @@ from ariadne.services import hermes_suggested_remediation as suggestion_field
DEFAULT_MAX_BODY_CHARS = 8000 DEFAULT_MAX_BODY_CHARS = 8000
UNDIAGNOSED = "undiagnosed" UNDIAGNOSED = "undiagnosed"
# A finding is not a build failure, and rendering one through the other's
# wording produces an issue that is wrong in every heading: a line number
# printed as a build, a "first failed gate" for a build that passed, and a
# SonarQube link labelled "failed build".
SONAR_PREFIX = "sonar/"
def is_finding(context: dict) -> bool:
"""Report whether this issue describes a finding rather than a failure."""
return str(context.get("incident_id") or "").startswith(SONAR_PREFIX)
_MAX_TITLE_CHARS = 120 _MAX_TITLE_CHARS = 120
_MAX_FACTS = 10 _MAX_FACTS = 10
@ -94,7 +105,10 @@ def issue_title(context: dict) -> str:
`[hermes] {job} #{build}: {classification}` clipped to 120 characters. `[hermes] {job} #{build}: {classification}` clipped to 120 characters.
""" """
prefix = f"[hermes] {context.get('job')} #{context.get('build_number')}: " if is_finding(context):
prefix = f"[hermes] {context.get('job')}: "
else:
prefix = f"[hermes] {context.get('job')} #{context.get('build_number')}: "
classification = str(context.get("classification") or UNDIAGNOSED) classification = str(context.get("classification") or UNDIAGNOSED)
room = _MAX_TITLE_CHARS - len(prefix) room = _MAX_TITLE_CHARS - len(prefix)
if room <= len(_ELLIPSIS): if room <= len(_ELLIPSIS):
@ -149,6 +163,11 @@ def _summary_line(context: dict) -> str:
incident_id = context.get("incident_id") incident_id = context.get("incident_id")
classification = context.get("classification") classification = context.get("classification")
if is_finding(context):
return (
f"SonarQube reports **{classification}** in `{context.get('finding_path')}`. "
"The build is green; this is a standing finding, not a failure."
)
run_id = str(context.get("run_id") or "") run_id = str(context.get("run_id") or "")
if not run_id or run_id == "unknown": if not run_id or run_id == "unknown":
# The headline is the first thing read, so it must not credit a model # The headline is the first thing read, so it must not credit a model
@ -178,14 +197,17 @@ def _footer(context: dict) -> str:
def _human_section(context: dict) -> str: def _human_section(context: dict) -> str:
"""Render the section explaining why the incident needs a person.""" """Render the section explaining why the incident needs a person."""
lines = ["## Why a human is needed", str(context.get("reason") or "no reason recorded")] heading = "## What is wrong" if is_finding(context) else "## Why a human is needed"
lines = [heading, str(context.get("reason") or "no reason recorded")]
observation = str(context.get("observation") or "").strip() observation = str(context.get("observation") or "").strip()
if observation: if observation:
# Escalations that never reached a model carry their explanation here # Escalations that never reached a model carry their explanation here
# rather than in facts, which are only populated from a diagnosis. # rather than in facts, which are only populated from a diagnosis.
lines.append(observation) lines.append(observation)
authorize_reason = str(context.get("authorize_reason") or "") authorize_reason = str(context.get("authorize_reason") or "")
if authorize_reason: if authorize_reason and is_finding(context):
lines.append(f"Ariadne opened no pull request for it: {authorize_reason}.")
elif authorize_reason:
lines.append(f"Ariadne did not authorize automated remediation: `{authorize_reason}`.") lines.append(f"Ariadne did not authorize automated remediation: `{authorize_reason}`.")
return "\n\n".join(lines) return "\n\n".join(lines)
@ -220,10 +242,13 @@ def _links_section(context: dict) -> str:
build_url = str(context.get("build_url") or "") build_url = str(context.get("build_url") or "")
proposal_url = str(context.get("code_proposal_url") or "") proposal_url = str(context.get("code_proposal_url") or "")
lines = [ if is_finding(context):
"## Links", lines = ["## Links", f"- SonarQube finding: {build_url}" if build_url else "- SonarQube finding: url unavailable"]
f"- Failed build: {build_url}" if build_url else "- Failed build: url unavailable", else:
] lines = [
"## Links",
f"- Failed build: {build_url}" if build_url else "- Failed build: url unavailable",
]
if proposal_url: if proposal_url:
lines.append(f"- Proposed fix awaiting review: {proposal_url}") lines.append(f"- Proposed fix awaiting review: {proposal_url}")
lines.append(f"- {_AUDIT_NOTE}") lines.append(f"- {_AUDIT_NOTE}")

View File

@ -209,6 +209,7 @@ def _context( # noqa: PLR0913 - the issue body needs every one of these
"classification": rule, "classification": rule,
"confidence": None, "confidence": None,
"first_failed_gate": "", "first_failed_gate": "",
"finding_path": issue.get("path"),
"reason": reason, "reason": reason,
# Says plainly why nobody patched it, so the issue does not read as a # Says plainly why nobody patched it, so the issue does not read as a
# failure of the automation. # failure of the automation.

View File

@ -232,3 +232,58 @@ def test_the_prompt_bounds_the_file_it_sends() -> None:
prompt = module._prompt(INCIDENT, dict(ISSUE), "x" * 90000) prompt = module._prompt(INCIDENT, dict(ISSUE), "x" * 90000)
assert len(prompt) < 45000 assert len(prompt) < 45000
def test_a_finding_issue_never_uses_build_failure_wording() -> None:
"""A line number printed as a build number reads as a failure that never happened."""
from ariadne.services import hermes_incident_body as body
from ariadne.services import hermes_code_suggestion as cs
context = {
"incident_id": INCIDENT,
"job": "ariadne",
"build_number": 42,
"classification": "python:S3776",
"finding_path": "ariadne/services/thing.py",
"reason": "The function branches five ways.",
"authorize_reason": "no automated patch was possible for this finding",
"build_url": "https://quality.example/project/issues?open=AZ1",
"run_id": "run_x",
"run_url": "https://agent.example/chat?resume=run_x",
"code_suggestions": cs.from_payload({"code_suggestions": [dict(SUGGESTION)]}),
}
assert body.issue_title(context) == "[hermes] ariadne: python:S3776"
rendered = body.issue_body(context)
assert rendered.startswith("SonarQube reports **python:S3776**")
assert "The build is green" in rendered
assert "## What is wrong" in rendered
assert "- SonarQube finding: https://quality.example" in rendered
assert "Failed build" not in rendered
assert "first failed gate" not in rendered
assert "Why a human is needed" not in rendered
def test_a_build_failure_issue_is_unchanged() -> None:
"""The finding wording must not leak into real triage issues."""
from ariadne.services import hermes_incident_body as body
context = {
"incident_id": "ariadne/408",
"job": "ariadne",
"build_number": 408,
"classification": "pytest_test_failure",
"reason": "a repository test failure",
"authorize_reason": "human_required",
"build_url": "https://ci.example/job/ariadne/408/",
"run_id": "run_y",
}
assert body.issue_title(context) == "[hermes] ariadne #408: pytest_test_failure"
rendered = body.issue_body(context)
assert "## Why a human is needed" in rendered
assert "- Failed build: https://ci.example" in rendered
assert "did not authorize automated remediation" in rendered
assert "SonarQube" not in rendered