From 7f37a36722803ec6eb2b8f074e7557a806152a87 Mon Sep 17 00:00:00 2001 From: Marcos Date: Sat, 22 Aug 2026 14:43:59 +0000 Subject: [PATCH] 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. --- pilot/ai_review.py | 19 ++++++++++++++++++- tests/pilot/test_ai_review.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/pilot/ai_review.py b/pilot/ai_review.py index b16a568..5c754c9 100644 --- a/pilot/ai_review.py +++ b/pilot/ai_review.py @@ -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** — 2–4 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) diff --git a/tests/pilot/test_ai_review.py b/tests/pilot/test_ai_review.py index 39acb91..859ef8d 100644 --- a/tests/pilot/test_ai_review.py +++ b/tests/pilot/test_ai_review.py @@ -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",