feat(ai_review): parse .pr-review.json:static_message + render as banner

Repos can pin a free-text notice (e.g. 'this repo is in maintenance mode',
or 'reviewers: focus on the public API only for this quarter') in
.pr-review.json:static_message. The string is stripped and capped at 400
chars (mirror of the existing instructions cap), then rendered as a
Markdown blockquote (> {msg}) directly under the REVIEW_HEADER so it
surfaces on every review without scrolling.

Plumbing: parse_repo_config exposes 'static_message'; format_review_body
accepts a static_message kwarg and inserts the blockquote before the
'### Summary of Changes' section; review_pr threads
config.get('static_message') to both call sites (salvage path + happy
path). Empty / non-string values are silently dropped, mirroring the
parser's 'ignore blank' handling for every other text field.
This commit is contained in:
Marcos
2026-08-22 14:43:59 +00:00
parent b6b8173ccb
commit 7f37a36722
2 changed files with 52 additions and 1 deletions
+18 -1
View File
@@ -215,12 +215,16 @@ def format_review_body(
risks: list[str] | None = None,
findings_for_table: list[dict] | None = None,
inline_count: int = 0,
static_message: str = "",
) -> str:
"""Format the posted review summary body.
Layout (per the operator's format guide):
* Header line (``🤖 AI Review …``).
* Optional static banner (``> {static_message}``) — repo-wide call-out
from `.pr-review.json:static_message`, placed under the header so
every reviewer sees it on every review without scrolling.
* **Summary of Changes** — 24 bullets of what the PR introduces
(`summary_changes`); falls back to the opencode prose `summary` if
the agent didn't emit the list.
@@ -241,6 +245,11 @@ def format_review_body(
header = REVIEW_HEADER.format(model=model, sha=sha[:8] if sha else "unknown")
parts: list[str] = [header]
# Optional free-text banner. Rendered as a Markdown blockquote immediately
# after the header — front-of-mind for any maintainer scanning the review.
if static_message and static_message.strip():
parts.append(f"> {static_message.strip()}")
# --- Summary of Changes ---
sc = list(summary_changes or [])
if not sc and summary:
@@ -1137,6 +1146,7 @@ CONFIG_MAX_ITEM_CHARS = 200
CONFIG_MAX_INSTRUCTIONS_CHARS = 4000
CONFIG_MAX_PATTERNS_ITEMS = 16 # allow + deny separately, total 32 entries
CONFIG_MAX_FINDINGS = 30
CONFIG_MAX_STATIC_MESSAGE_CHARS = 400 # free-text banner, mirror of instructions
STYLES = frozenset(STYLE_DEFAULTS)
SEVERITY_VALUES = frozenset(SEVERITIES)
@@ -1152,6 +1162,7 @@ def parse_repo_config(raw: str) -> dict:
Recognised keys (all optional):
focus, exclude_paths, languages, instructions — text steer
static_message ≤ CONFIG_MAX_STATIC_MESSAGE_CHARS — banner under header
style strict|balanced|lenient — default: balanced
severity_threshold low|medium|high|critical — default: per style
max_findings 1..CONFIG_MAX_FINDINGS — default: per style
@@ -1185,6 +1196,10 @@ def parse_repo_config(raw: str) -> dict:
if isinstance(instr, str) and instr.strip():
out["instructions"] = instr.strip()[:CONFIG_MAX_INSTRUCTIONS_CHARS]
sm = data.get("static_message")
if isinstance(sm, str) and sm.strip():
out["static_message"] = sm.strip()[:CONFIG_MAX_STATIC_MESSAGE_CHARS]
style = data.get("style")
if isinstance(style, str) and style.strip().lower() in STYLES:
out["style"] = style.strip().lower()
@@ -1945,7 +1960,8 @@ def review_pr(
usage_section = _render_collapsible_usage(usage, display_model, config=config) if report_usage else ""
post_review(api, repo, index, token, format_review_body(
salvaged or "AI review produced no parseable output.",
display_model, sha, usage_section=usage_section))
display_model, sha, usage_section=usage_section,
static_message=(config or {}).get("static_message", "")))
return True
else:
user_prompt = build_user_prompt(title, body + compression_note, diff, config, prior, additional_context)
@@ -2015,6 +2031,7 @@ def review_pr(
risks=risks,
findings_for_table=findings,
inline_count=len(anchored),
static_message=(config or {}).get("static_message", ""),
)
post_inline_review(api, repo, index, token, summary_body, anchored)
+34
View File
@@ -401,6 +401,24 @@ def test_parse_repo_config_partial_and_bad():
assert parse_repo_config('{"instructions":" "}') == {}
def test_parse_repo_config_reads_static_message():
cfg = parse_repo_config(json.dumps({"static_message": " NOTE: this repo is in maintenance mode "}))
assert cfg == {"static_message": "NOTE: this repo is in maintenance mode"}
def test_parse_repo_config_static_message_caps_length():
long_text = "x" * 9999
cfg = parse_repo_config(json.dumps({"static_message": long_text}))
assert "static_message" in cfg
assert len(cfg["static_message"]) <= 400
def test_parse_repo_config_static_message_ignores_blank():
assert "static_message" not in parse_repo_config(json.dumps({"static_message": " "}))
assert "static_message" not in parse_repo_config(json.dumps({"static_message": ""}))
assert "static_message" not in parse_repo_config(json.dumps({"static_message": 42}))
# ---------------------------------------------------------------------------
# dedupe / prior-context parsing
# ---------------------------------------------------------------------------
@@ -823,6 +841,22 @@ def test_format_review_body_no_usage_section_omitted():
assert "AI usage" not in body
def test_format_review_body_renders_static_message_banner():
body = format_review_body(
"", "glm-5.2:cloud", "abcdef1234567890",
static_message="NOTE: this repo is in maintenance mode.",
)
assert "> NOTE: this repo is in maintenance mode." in body
# Banner sits under the header and above the rest of the body.
assert body.index("NOTE") > body.index("🤖")
assert body.index("NOTE") < body.index("### Summary of Changes")
def test_format_review_body_omits_static_message_when_blank():
body = format_review_body("", "glm-5.2:cloud", "abcdef1234567890")
assert "> " not in body
def test_format_review_body_with_summary_changes_and_risks():
body = format_review_body(
"", "glm-5.2:cloud", "abcdef1234567890",