fix(review): correct diff-compression line numbers, prior-review dedupe, triage skip
Four defects, all found reviewing PR #9 (two of them by pragent-bot's own review of that PR, which the anchoring bug then misplaced): * compress_diff dropped context lines but copied the original `@@` hunk header verbatim, so the header no longer described the lines beneath it. parse_diff_anchors then walked stale headers and produced anchor sets shifted by the number of elided lines, misplacing inline comments or demoting them to bullets. Each surviving run of lines is now re-emitted as its own hunk with a recomputed `@@ -a,b +c,d @@`, so the output stays a valid unified diff whose numbers describe the real post-change file. The pseudo-marker `@@ … N context line(s) omitted … @@` is gone; it parsed as a hunk header and reset the anchor counter to 0. Anchoring additionally runs on the raw diff now, so the prompt window can never shrink the anchorable set. * compress_diff's `_FILE_HEADER` regex matched diff *body* lines: a removed YAML `---` separator or an added `++` line was read as a file header, truncating the hunk and dropping its `@@` header with it. Body detection is now prefix-based, with a full-shape hunk-header regex. * extract_finding_bullets could not match the bullets pragent itself posts: summary_bullets renders an emoji severity badge between the `-` and the `[SEV]` tag, which the regex rejected, so compact_prior_reviews always returned [] and every re-review repeated its previous findings. * triage returning `{"lenses":[]}` — documented in .opencode/agents/triage.md as "no lens has surface, skip the fan-out" — ran every lens instead, since _intersect_with_triage mapped an empty selection to "all" and the call site had a second `or reviewers` fallback. `[]` and None are now distinct outcomes: `[]` skips, None fails open. A roster naming only unknown lens ids now fails open rather than silencing the review. The skip path returns a well-formed empty-findings response instead of "", which had landed in ai_review's unparseable-output branch and posted "AI review produced no parseable output" — a malfunction message for a normal verdict. Also: non-URL references (a CVE id, a doc title) rendered as `[CVE-2024-1234](CVE-2024-1234)`, a broken relative link in Gitea — now plain text. PRAGENT_DIFF_CONTEXT and friends parse through _int_env, so a typo logs and falls back instead of killing a review mid-flight. Removed format_usage_section, dead since the collapsible usage block replaced it and carrying a duplicate copy of the price-target logic. Tests: 290 -> 301. New coverage for hunk-header fidelity before/after compression, header-shaped content lines, the bullet round-trip against the real renderer, and triage's three outcomes (previously untested). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B11e8TZZxJyzHW7jj7KWUN
This commit is contained in:
@@ -1084,8 +1084,18 @@ def triage(
|
||||
default_model: str,
|
||||
factory_root: str,
|
||||
) -> list[str] | None:
|
||||
"""Run the triage agent. Returns the lens subset with surface, or None to
|
||||
mean "all reviewers" (fail-open on any error).
|
||||
"""Run the triage agent. Returns the lens subset with surface.
|
||||
|
||||
Three outcomes, kept distinct on purpose:
|
||||
|
||||
* ``[lens, …]`` — run exactly these.
|
||||
* ``[]`` — the agent deliberately returned an empty list: no lens
|
||||
has surface on this diff, so the fan-out is skipped entirely. Only a
|
||||
literally-empty ``lenses`` list produces this.
|
||||
* ``None`` — fail open, run everything. Covers triage disabled, a
|
||||
crash, unparseable output, a malformed `lenses` value, AND the case
|
||||
where the agent named only ids that don't exist (a hallucinated roster
|
||||
is not a verdict of "nothing to review").
|
||||
|
||||
`triage_cfg.enabled = False` → skip triage, return None.
|
||||
"""
|
||||
@@ -1125,7 +1135,20 @@ def triage(
|
||||
lenses = obj.get("lenses")
|
||||
if not isinstance(lenses, list):
|
||||
return None
|
||||
if not lenses:
|
||||
# Deliberate "no lens needed" verdict — the one case that skips.
|
||||
print("pragent: triage selected no lenses (no review surface)", flush=True)
|
||||
return []
|
||||
valid = [lid for lid in lenses if isinstance(lid, str) and lid in lens_ids]
|
||||
if not valid:
|
||||
# The agent named lenses, but none of them exist. That's a bad roster,
|
||||
# not an empty one — fail open rather than silently skipping the review.
|
||||
print(
|
||||
f"pragent: triage named no known lenses ({lenses!r}); "
|
||||
f"falling back to all lenses",
|
||||
flush=True,
|
||||
)
|
||||
return None
|
||||
cap = triage_cfg.get("max_lenses", 5)
|
||||
selected = valid[:cap]
|
||||
print(f"pragent: triage selected {selected}", flush=True)
|
||||
@@ -1133,13 +1156,19 @@ def triage(
|
||||
|
||||
|
||||
def _intersect_with_triage(
|
||||
reviewers: list[ReviewerSpec], selected_ids: list[str]
|
||||
reviewers: list[ReviewerSpec], selected_ids: list[str] | None
|
||||
) -> list[ReviewerSpec]:
|
||||
"""Filter `reviewers` to those named by `selected_ids`, preserving the
|
||||
original order. Lenses in `selected_ids` not present in `reviewers` are
|
||||
dropped silently. `None` or empty list → no triage, return all."""
|
||||
if not selected_ids:
|
||||
return list(reviewers)
|
||||
dropped silently.
|
||||
|
||||
An empty `selected_ids` yields an empty result — "triage picked nothing"
|
||||
is a real verdict and the caller short-circuits on it. Fail-open is
|
||||
signalled by `triage()` returning None, never by an empty list; conflating
|
||||
the two made a "no review surface" verdict run every lens instead.
|
||||
"""
|
||||
if selected_ids is None:
|
||||
return list(reviewers) # fail-open: triage produced no verdict
|
||||
sel = set(selected_ids)
|
||||
return [r for r in reviewers if r.id in sel]
|
||||
|
||||
@@ -1234,10 +1263,18 @@ def run_lenses_review(
|
||||
workdir, triage_cfg, reviewers, model, _factory_dir(),
|
||||
)
|
||||
if selected is not None:
|
||||
reviewers = _intersect_with_triage(reviewers, selected) or reviewers
|
||||
if not selected:
|
||||
# Triage says nothing here has review surface. Skip the
|
||||
# fan-out and post a clean empty review — running all N
|
||||
# lenses anyway would burn N subprocesses to contradict it.
|
||||
return _no_surface_response(repo, index, sha, len(reviewers))
|
||||
reviewers = _intersect_with_triage(reviewers, selected)
|
||||
|
||||
if not reviewers:
|
||||
return "", None
|
||||
# Every lens was filtered out (skip_if_all_changed_paths, or a
|
||||
# triage subset naming lenses this repo doesn't enable). Same
|
||||
# outcome as the triage skip: nothing to run, nothing to say.
|
||||
return _no_surface_response(repo, index, sha, 0)
|
||||
|
||||
factory_root = _factory_dir()
|
||||
results = run_lenses(workdir, reviewers, model, factory_root)
|
||||
@@ -1282,6 +1319,37 @@ def run_lenses_review(
|
||||
shutil.rmtree(workdir, ignore_errors=True)
|
||||
|
||||
|
||||
def _no_surface_response(
|
||||
repo: str, index: str, sha: str, n_lenses: int
|
||||
) -> tuple[str, dict | None]:
|
||||
"""A well-formed 'nothing to review' result for the no-lens paths.
|
||||
|
||||
Returns the same shape every other path returns — prose plus a final
|
||||
```json fence with an empty `findings` array — so
|
||||
`ai_review.parse_review_output` parses it normally. Returning bare `""`
|
||||
here (the old behaviour) landed in ai_review's unparseable-output branch
|
||||
and posted "AI review produced no parseable output", which reads as a
|
||||
malfunction rather than a verdict.
|
||||
"""
|
||||
if n_lenses:
|
||||
summary = (
|
||||
f"Triage found no review surface in {repo}#{index} "
|
||||
f"(sha {sha[:8]}): none of the {n_lenses} configured lens(es) "
|
||||
f"apply to this diff. No findings."
|
||||
)
|
||||
else:
|
||||
summary = (
|
||||
f"No lens applies to {repo}#{index} (sha {sha[:8]}) after path "
|
||||
f"filtering. No findings."
|
||||
)
|
||||
text = (
|
||||
f"{summary}\n\n"
|
||||
f"## Findings (multi-lens)\n\n"
|
||||
f"```json\n{json.dumps({'summary': summary, 'findings': []}, indent=2)}\n```\n"
|
||||
)
|
||||
return text, None
|
||||
|
||||
|
||||
def _fallback_single_primary(workdir: str, model: str) -> tuple[str, dict | None]:
|
||||
"""Used when reviewers[] resolves to empty (all activation:off)."""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user