fix(hermes): stop telling the patcher a build failed when none did
All checks were successful
Tests / Declarative: Post Actions passed: 1355
All checks were successful
Tests / Declarative: Post Actions passed: 1355
The patch prompt was written for one entry point and inherited whole by the other. Rendered for a quality sweep it opened with "Use $triage-titan-test-failures", called the findings "defects the build reported", and labelled the payload "Failing test evidence bundle" - three false statements in a row, on a green build, with an empty jenkins section sitting right below them. The instruction two lines later already said the build was not failing, so the prompt contradicted itself. Wrong in the first line the model reads is the expensive kind of wrong: it frames everything after it. The skill mention is the concrete cost - it sends Hermes to investigate a build that did not fail, spending a tool call to explain a failure that does not exist, when the finding and the file are already inlined below it. The framing now follows the bundle's origin. A bundle carrying both static analysis and build evidence is treated as build-driven, because a real failure is the more urgent framing of the two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
63a8abb743
commit
ca545fa75f
@ -342,7 +342,16 @@ def defect_instruction(defects: list[dict[str, Any]]) -> str:
|
||||
|
||||
if not defects:
|
||||
return ""
|
||||
lines: list[str] = ["The build reported these specific defects; address exactly these:"]
|
||||
# A static-analysis sweep runs against a green build. Saying "the build
|
||||
# reported" there would be a plain falsehood in the first line the model
|
||||
# reads, and the instruction two lines later already contradicts it.
|
||||
from_build = any(defect["category"] != SONARQUBE_ISSUE for defect in defects)
|
||||
lead = (
|
||||
"The build reported these specific defects; address exactly these:"
|
||||
if from_build
|
||||
else "Static analysis reported these findings; address exactly these:"
|
||||
)
|
||||
lines: list[str] = [lead]
|
||||
for defect in defects:
|
||||
lines.append(_defect_line(defect))
|
||||
if any(d["category"] == LINT_VIOLATION for d in defects):
|
||||
|
||||
@ -12,7 +12,7 @@ import json
|
||||
from . import hermes_code_defects
|
||||
|
||||
|
||||
_PATCH_PROMPT_TEMPLATE = """Use $triage-titan-test-failures.
|
||||
_PATCH_PROMPT_TEMPLATE = """__PREAMBLE__
|
||||
You are proposing a MINIMAL source fix for incident __INCIDENT_ID__.
|
||||
The repository is __OWNER__/__REPO__ branch __BASE_BRANCH__.
|
||||
Return ONLY a single JSON object with exactly these keys and no others:
|
||||
@ -24,18 +24,55 @@ Change as few lines as possible; do not reformat; do not add dependencies.
|
||||
Ariadne validates and pushes the change — you do not execute anything.
|
||||
Set human_required to true if the fix is not a small localized source change.
|
||||
__DEFECT_INSTRUCTION__
|
||||
Failing test evidence bundle:
|
||||
__BUNDLE_LABEL__
|
||||
__BUNDLE__
|
||||
|
||||
__FILE_SECTIONS__"""
|
||||
|
||||
_FILE_SECTION_TEMPLATE = "Current content of the candidate file {path}:\n{contents}"
|
||||
|
||||
# A build-driven repair starts from a failure worth investigating, so the
|
||||
# triage skill earns its cost. A quality sweep does not: no build failed, there
|
||||
# is no console to read, and the file is already inlined below. Sending Hermes
|
||||
# to investigate a green build would waste a tool call and invite it to explain
|
||||
# a failure that does not exist.
|
||||
_BUILD_PREAMBLE = "Use $triage-titan-test-failures."
|
||||
_SWEEP_PREAMBLE = (
|
||||
"A static-analysis finding, not a build failure. Nothing is broken and no "
|
||||
"investigation is needed: the finding and the file are below."
|
||||
)
|
||||
_BUILD_BUNDLE_LABEL = "Failing test evidence bundle:"
|
||||
_SWEEP_BUNDLE_LABEL = "Static analysis evidence bundle:"
|
||||
|
||||
|
||||
def is_quality_sweep(bundle: dict) -> bool:
|
||||
"""Report whether a bundle came from a quality sweep rather than a build.
|
||||
|
||||
Inputs: the evidence bundle. Outputs: True when it carries SonarQube
|
||||
findings and no build evidence. A bundle with both is treated as
|
||||
build-driven, since a real failure is the more urgent framing.
|
||||
"""
|
||||
|
||||
if not isinstance(bundle, dict):
|
||||
return False
|
||||
sonar = bundle.get("sonarqube")
|
||||
if not isinstance(sonar, dict) or not sonar.get("issues"):
|
||||
return False
|
||||
jenkins = bundle.get("jenkins") if isinstance(bundle.get("jenkins"), dict) else {}
|
||||
return not (
|
||||
jenkins.get("console_failures") or jenkins.get("console_tail") or jenkins.get("failed_tests")
|
||||
)
|
||||
|
||||
|
||||
def build_patch_prompt(
|
||||
incident_id: str, bundle: dict, code_cfg: dict, fetched: dict[str, str]
|
||||
) -> str:
|
||||
"""Render the frozen patch prompt with incident, bundle, and file context."""
|
||||
"""Render the frozen patch prompt with incident, bundle, and file context.
|
||||
|
||||
The framing follows the bundle's origin. A prompt that opens by telling the
|
||||
model it is looking at a failing test, when the build is green and the
|
||||
evidence is a static-analysis finding, is wrong in the first line it reads.
|
||||
"""
|
||||
|
||||
compact = json.dumps(bundle, separators=(",", ":"), ensure_ascii=True)
|
||||
listing = "\n".join(f"- {path}" for path in fetched)
|
||||
@ -46,8 +83,13 @@ def build_patch_prompt(
|
||||
_FILE_SECTION_TEMPLATE.format(path=path, contents=contents)
|
||||
for path, contents in fetched.items()
|
||||
)
|
||||
sweep = is_quality_sweep(bundle)
|
||||
return (
|
||||
_PATCH_PROMPT_TEMPLATE.replace("__INCIDENT_ID__", incident_id)
|
||||
_PATCH_PROMPT_TEMPLATE.replace(
|
||||
"__PREAMBLE__", _SWEEP_PREAMBLE if sweep else _BUILD_PREAMBLE
|
||||
)
|
||||
.replace("__BUNDLE_LABEL__", _SWEEP_BUNDLE_LABEL if sweep else _BUILD_BUNDLE_LABEL)
|
||||
.replace("__INCIDENT_ID__", incident_id)
|
||||
.replace("__OWNER__", str(code_cfg.get("owner") or ""))
|
||||
.replace("__REPO__", str(code_cfg.get("repo") or ""))
|
||||
.replace("__BASE_BRANCH__", str(code_cfg.get("base_branch") or ""))
|
||||
|
||||
@ -307,3 +307,56 @@ def test_the_scheduled_entry_point_runs_the_sweep_when_enabled(monkeypatch) -> N
|
||||
"api_key": "k",
|
||||
"total_timeout_seconds": 420.0,
|
||||
}
|
||||
|
||||
|
||||
def test_the_sweep_prompt_never_claims_a_build_failed() -> None:
|
||||
"""A green build framed as a failing test is wrong in the first line read."""
|
||||
|
||||
from ariadne.services import hermes_code_prompt
|
||||
|
||||
cfg = {"owner": "bstein", "repo": "ariadne", "base_branch": "master"}
|
||||
prompt = hermes_code_prompt.build_patch_prompt(
|
||||
"sonar/ariadne/AZ-1",
|
||||
module.bundle_for("ariadne", _issue()),
|
||||
{**cfg, "allowed_path_prefixes": ["ariadne/"], "allowed_suffixes": [".py"]},
|
||||
{"ariadne/services/hermes_code_defects.py": "x"},
|
||||
)
|
||||
|
||||
assert prompt.startswith("A static-analysis finding, not a build failure.")
|
||||
assert "Failing test evidence bundle" not in prompt
|
||||
assert "Static analysis evidence bundle:" in prompt
|
||||
assert "The build reported these specific defects" not in prompt
|
||||
# The triage skill investigates a failed build; there isn't one.
|
||||
assert "$triage-titan-test-failures" not in prompt
|
||||
|
||||
|
||||
def test_a_build_driven_prompt_is_unchanged() -> None:
|
||||
from ariadne.services import hermes_code_prompt
|
||||
|
||||
bundle = {"jenkins": {"console_tail": "AssertionError", "console_failures": [], "failed_tests": []}}
|
||||
prompt = hermes_code_prompt.build_patch_prompt(
|
||||
"ariadne/9", bundle, {"owner": "bstein", "repo": "ariadne"}, {"ariadne/a.py": "x"}
|
||||
)
|
||||
|
||||
assert prompt.startswith("Use $triage-titan-test-failures.")
|
||||
assert "Failing test evidence bundle:" in prompt
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("bundle", "expected"),
|
||||
[
|
||||
({"sonarqube": {"issues": [{"path": "a.py"}]}, "jenkins": {}}, True),
|
||||
({"sonarqube": {"issues": []}, "jenkins": {}}, False),
|
||||
({"jenkins": {"console_tail": "boom"}}, False),
|
||||
# Both present: a real failure is the more urgent framing.
|
||||
(
|
||||
{"sonarqube": {"issues": [{"path": "a.py"}]}, "jenkins": {"console_tail": "boom"}},
|
||||
False,
|
||||
),
|
||||
("not-a-dict", False),
|
||||
],
|
||||
)
|
||||
def test_a_bundles_origin_decides_the_framing(bundle, expected) -> None:
|
||||
from ariadne.services import hermes_code_prompt
|
||||
|
||||
assert hermes_code_prompt.is_quality_sweep(bundle) is expected
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user