Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
40e5188
Add review risk assessment and optional repository review rules
xiaodeng666a May 14, 2026
104a696
Remove local review history from this PR
xiaodeng666a May 14, 2026
5a74da3
Fix help header text and trailing whitespace
xiaodeng666a May 14, 2026
fc8ee1d
Trigger bot review again
xiaodeng666a May 15, 2026
d8cbb89
Validate max review rules tokens config
xiaodeng666a May 15, 2026
e237f87
Fix review rules token limit parsing
xiaodeng666a May 15, 2026
13136c6
Fix review rules warning indentation
xiaodeng666a May 15, 2026
03e0a0d
Remove review priority files from YAML fallback keys
xiaodeng666a May 15, 2026
2d099fa
Align structured review sections with shared emoji rendering
xiaodeng666a May 15, 2026
71011b3
Load review rules from trusted base ref
xiaodeng666a May 15, 2026
666a265
Guard review priority files rendering
xiaodeng666a May 15, 2026
8c93ceb
Harden priority files rendering and yaml repair keys
xiaodeng666a May 15, 2026
15d8940
Support GitLab base ref for review rules
xiaodeng666a May 15, 2026
20a256d
Validate review rules paths config
xiaodeng666a May 15, 2026
b71e7be
Trigger review refresh
xiaodeng666a May 15, 2026
b97e833
Disable new review fields by default and remove BOM
xiaodeng666a May 15, 2026
983d227
Add provider-level trusted base ref for review rules
xiaodeng666a May 15, 2026
9298146
Add review rules coverage and structured review field tests
xiaodeng666a May 15, 2026
20673da
Trigger review refresh again
xiaodeng666a May 15, 2026
d2f6982
Trigger review refresh after latest fixes
xiaodeng666a May 15, 2026
6020941
Add Bitbucket Server review rules file loading
xiaodeng666a May 15, 2026
ca9ab5e
Trigger review refresh once more
xiaodeng666a May 15, 2026
c133868
Add Gitea review rules file loading
xiaodeng666a May 15, 2026
b2b3f6a
Trigger review refresh after latest provider fixes
xiaodeng666a May 15, 2026
fecf295
Trigger review refresh once more
xiaodeng666a May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions pr_agent/algo/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,38 @@ def convert_to_markdown_v2(output_data: dict,
markdown_text += f"### {emoji} Security concerns\n\n"
value = emphasize_header(value.strip(), only_markdown=True)
markdown_text += f"{value}\n\n"
elif 'risk level' in key_nice.lower():
risk_value = str(value).strip().lower().replace('_', ' ')
risk_display = risk_value.capitalize() if risk_value else 'Unknown'
if gfm_supported:
markdown_text += f"<tr><td><strong>Risk level</strong>: {risk_display}</td></tr>\n"
else:
markdown_text += f"### Risk level: {risk_display}\n\n"
elif 'merge recommendation' in key_nice.lower():
recommendation = str(value).strip().replace('_', ' ')
recommendation_display = recommendation.capitalize() if recommendation else 'Unknown'
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated
if gfm_supported:
markdown_text += f"<tr><td><strong>Merge recommendation</strong>: {recommendation_display}</td></tr>\n"
else:
markdown_text += f"### Merge recommendation: {recommendation_display}\n\n"
elif 'review priority files' in key_nice.lower():
if gfm_supported:
markdown_text += "<tr><td>"
if not value:
markdown_text += "<strong>Priority files</strong>: None"
else:
markdown_text += "<strong>Priority files</strong><br><br>"
for priority_file in value:
markdown_text += f"- {priority_file}<br>"
markdown_text += "</td></tr>\n"
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated
else:
if not value:
markdown_text += "### Priority files: None\n\n"
else:
markdown_text += "### Priority files\n\n"
for priority_file in value:
markdown_text += f"- {priority_file}\n"
markdown_text += "\n"
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated
elif 'todo sections' in key_nice.lower():
if gfm_supported:
markdown_text += "<tr><td>"
Expand Down
6 changes: 6 additions & 0 deletions pr_agent/settings/configuration.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ require_security_review=true
require_estimate_contribution_time_cost=false
require_todo_scan=false
require_ticket_analysis_review=true
require_risk_assessment=true
require_merge_recommendation=true
require_priority_files=true
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated
enable_review_rules=true
review_rules_paths=[".pr_agent/review_rules.md",".github/review_rules.md","docs/review_rules.md"]
max_review_rules_tokens=1200
Comment on lines +84 to +89
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. .pr_agent.toml missing new keys 📘 Rule violation ⚙ Maintainability

New /review configuration knobs were added to pr_agent/settings/configuration.toml, but the
mirrored root .pr_agent.toml was not updated to include these options, risking divergence and user
confusion about available/default behavior. This violates the mirror-sync requirement when
behavior/config surface area changes.
Agent Prompt
## Issue description
New `pr_reviewer` settings were added in `pr_agent/settings/configuration.toml` but are not reflected in the root `.pr_agent.toml` mirror, making mirrored configuration sources inconsistent.

## Issue Context
The repo uses both a packaged default config (`pr_agent/settings/*.toml`) and a root `.pr_agent.toml` as a mirror/example for behavior changes.

## Fix Focus Areas
- pr_agent/settings/configuration.toml[84-89]
- .pr_agent.toml[1-20]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

# general options
publish_output_no_suggestions=true # Set to "false" if you only need the reviewer's remarks (not labels, not "security audit", etc.) and want to avoid noisy "No major issues detected" comments.
persistent_comment=true
Expand Down
44 changes: 44 additions & 0 deletions pr_agent/settings/pr_reviewer_prompts.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,16 @@ Extra instructions from the user:
{{ extra_instructions }}
======
{% endif %}
{%- if review_rules %}

Repository/team review rules:
======
{{ review_rules }}
======
- Apply these repository rules in addition to the general review criteria.
- When a repository rule conflicts with a generic style preference, prioritize the repository rule.
- Use the repository rules to decide what deserves extra attention, but do not invent violations that are not supported by the diff.
{% endif %}

The output must be a YAML object equivalent to type $PRReview, according to the following Pydantic definitions:
=====
Expand Down Expand Up @@ -117,6 +126,15 @@ class Review(BaseModel):
{%- if require_estimate_effort_to_review %}
estimated_effort_to_review_[1-5]: int = Field(description="Estimate, on a scale of 1-5 (inclusive), the time and effort required to review this PR by an experienced and knowledgeable developer. 1 means short and easy review, 5 means long and hard review. Take into account the size, complexity, quality, and the needed changes of the PR code diff.")
{%- endif %}
{%- if require_risk_assessment %}
risk_level: str = Field(description="Overall risk level of this PR. Answer with exactly one of: low, medium, high. Use high only when the PR introduces a clear bug, security concern, or major logic risk. Use medium when the PR is not clearly broken but contains non-trivial areas that require careful human verification. Use low when the PR is small, low-impact, and no important issues are identified.")
{%- endif %}
{%- if require_merge_recommendation %}
merge_recommendation: str = Field(description="Overall merge recommendation for this PR. Answer with exactly one of: safe_to_merge, merge_with_caution, changes_required. Use changes_required when there are clear issues that should be fixed before merge. Use merge_with_caution when the PR seems acceptable but still deserves focused reviewer attention. Use safe_to_merge when no important blockers or risks are identified.")
{%- endif %}
{%- if require_priority_files %}
review_priority_files: List[str] = Field(description="A short list of the most important files a human reviewer should inspect first. Return an empty list if the PR is too small or no file deserves special attention.")
{%- endif %}
{%- if require_estimate_contribution_time_cost %}
contribution_time_cost_estimate: ContributionTimeCostEstimate = Field(description="An estimate of the time required to implement the changes, based on the quantity, quality, and complexity of the contribution, as well as the context from the PR description and commit messages.")
{%- endif %}
Expand Down Expand Up @@ -165,6 +183,19 @@ review:
estimated_effort_to_review_[1-5]: |
3
{%- endif %}
{%- if require_risk_assessment %}
risk_level: |
low
{%- endif %}
{%- if require_merge_recommendation %}
merge_recommendation: |
safe_to_merge
{%- endif %}
{%- if require_priority_files %}
review_priority_files:
- |
src/example.py
{%- endif %}
{%- if require_score %}
score: 89
{%- endif %}
Expand Down Expand Up @@ -303,6 +334,19 @@ review:
estimated_effort_to_review_[1-5]: |
3
{%- endif %}
{%- if require_risk_assessment %}
risk_level: |
low
{%- endif %}
{%- if require_merge_recommendation %}
merge_recommendation: |
safe_to_merge
{%- endif %}
{%- if require_priority_files %}
review_priority_files:
- |
src/example.py
{%- endif %}
{%- if require_score %}
score: 89
{%- endif %}
Expand Down
55 changes: 52 additions & 3 deletions pr_agent/tools/pr_reviewer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import copy
import datetime
import traceback



from collections import OrderedDict
Comment on lines +4 to 5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Extra blank lines in imports 📘 Rule violation ⚙ Maintainability

The PR introduces three consecutive blank lines within the import block, which is likely to fail
Ruff/PEP8 blank-line rules and create unnecessary diff noise. This can break linting CI and violates
the repository formatting conventions.
Agent Prompt
## Issue description
There are too many consecutive blank lines inside the imports section, which can violate Ruff/PEP8 formatting rules.

## Issue Context
The file currently has three blank lines between `import traceback` and the next import group.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[4-7]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

from functools import partial
from typing import List, Tuple
Expand All @@ -13,7 +16,7 @@
get_pr_diff,
retry_with_fallback_models)
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import (ModelType, PRReviewHeader,
from pr_agent.algo.utils import (ModelType, PRReviewHeader, clip_tokens,
convert_to_markdown_v2, github_action_output,
load_yaml, show_relevant_configurations)
from pr_agent.config_loader import get_settings
Expand Down Expand Up @@ -66,6 +69,8 @@ def __init__(self, pr_url: str, is_answer: bool = False, is_auto: bool = False,
answer_str, question_str = self._get_user_answers()
self.pr_description, self.pr_description_files = (
self.git_provider.get_pr_description(split_changes_walkthrough=True))
self.review_rules = self._get_review_rules()

if (self.pr_description_files and get_settings().get("config.is_auto_command", False) and
get_settings().get("config.enable_ai_metadata", False)):
add_ai_metadata_to_diff_files(self.git_provider, self.pr_description_files)
Expand All @@ -85,6 +90,9 @@ def __init__(self, pr_url: str, is_answer: bool = False, is_auto: bool = False,
"require_score": get_settings().pr_reviewer.require_score_review,
"require_tests": get_settings().pr_reviewer.require_tests_review,
"require_estimate_effort_to_review": get_settings().pr_reviewer.require_estimate_effort_to_review,
"require_risk_assessment": get_settings().pr_reviewer.get("require_risk_assessment", False),
"require_merge_recommendation": get_settings().pr_reviewer.get("require_merge_recommendation", False),
"require_priority_files": get_settings().pr_reviewer.get("require_priority_files", False),
"require_estimate_contribution_time_cost": get_settings().pr_reviewer.require_estimate_contribution_time_cost,
'require_can_be_split_review': get_settings().pr_reviewer.require_can_be_split_review,
'require_security_review': get_settings().pr_reviewer.require_security_review,
Expand All @@ -99,6 +107,8 @@ def __init__(self, pr_url: str, is_answer: bool = False, is_auto: bool = False,
"related_tickets": get_settings().get('related_tickets', []),
'duplicate_prompt_examples': get_settings().config.get('duplicate_prompt_examples', False),
"date": datetime.datetime.now().strftime('%Y-%m-%d'),
"review_rules": self.review_rules,

}

self.token_handler = TokenHandler(
Expand All @@ -117,6 +127,41 @@ def parse_incremental(self, args: List[str]):
incremental = IncrementalPR(is_incremental)
return incremental

def _get_review_rules(self) -> str:
if not get_settings().pr_reviewer.get("enable_review_rules", False):
return ""

rule_paths = get_settings().pr_reviewer.get("review_rules_paths", []) or []
if isinstance(rule_paths, str):
rule_paths = [rule_paths]

Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
ref = getattr(getattr(self.git_provider, 'pr', None), 'head', None)
ref = getattr(ref, 'sha', None) or self.git_provider.get_pr_branch()
loaded_rules = []
loaded_rule_paths = []

for rule_path in rule_paths:
try:
rule_content = self.git_provider.get_pr_file_content(rule_path, ref)
except Exception:
continue
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.

if rule_content and rule_content.strip():
loaded_rule_paths.append(rule_path)
loaded_rules.append(f"File: `{rule_path}`\n{rule_content.strip()}")
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated

if not loaded_rules:
get_logger().info("No review rules file found for this PR")
return ""

review_rules = "\n\n---\n\n".join(loaded_rules)
max_tokens = get_settings().pr_reviewer.get("max_review_rules_tokens", 0)
if max_tokens and int(max_tokens) > 0:
review_rules = clip_tokens(review_rules, int(max_tokens))
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated

get_logger().info("Loaded review rules for this PR", artifacts={"rule_files": loaded_rule_paths})
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
return review_rules
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.

async def run(self) -> None:
try:
if not self.git_provider.get_files():
Expand Down Expand Up @@ -173,7 +218,7 @@ async def run(self) -> None:
if get_settings().pr_reviewer.persistent_comment and not self.incremental.is_incremental:
final_update_message = get_settings().pr_reviewer.final_update_message
self.git_provider.publish_persistent_comment(pr_review,
initial_header=f"{PRReviewHeader.REGULAR.value} 🔍",
initial_header=f"{PRReviewHeader.REGULAR.value}",
update_header=True,
final_update_message=final_update_message, )
Comment on lines 238 to 243
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Persistent header string mismatch 🐞 Bug ≡ Correctness

PRReviewer now uses an initial_header without the "🔍" suffix while convert_to_markdown_v2()
still emits a header that includes it. In persistent-comment mode this causes
publish_persistent_comment_full() to replace only the prefix and leave a stray "🔍" line in the
updated comment body.
Agent Prompt
## Issue description
Persistent comment updates rely on exact string matching/replacement of the review header. The PR changed `initial_header` passed to `publish_persistent_comment(...)` to omit the "🔍" suffix, but the generated markdown header still includes it, causing a dangling emoji line after header update.

## Issue Context
- `convert_to_markdown_v2()` emits a header with "🔍".
- `publish_persistent_comment_full()` updates an existing comment by `startswith(initial_header)` and then doing `pr_comment.replace(initial_header, updated_header)`.
- If `initial_header` is a prefix of the generated header (missing the emoji), the replacement leaves the emoji behind.

## Fix Focus Areas
- pr_agent/tools/pr_reviewer.py[233-240]
- pr_agent/algo/utils.py[159-166]
- pr_agent/git_providers/git_provider.py[301-316]

## Suggested fix
Choose one of:
1) Restore `initial_header` to exactly match the generated header (include " 🔍"), or
2) Remove the emoji from the generated header in `convert_to_markdown_v2()` and keep `initial_header` emoji-free, or
3) Centralize header construction (single helper/constant) so both generation and persistent-update use the exact same string.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

else:
Expand Down Expand Up @@ -234,14 +279,17 @@ def _prepare_pr_review(self) -> str:
first_key = 'review'
last_key = 'security_concerns'
data = load_yaml(self.prediction.strip(),
keys_fix_yaml=["ticket_compliance_check", "estimated_effort_to_review_[1-5]:", "security_concerns:", "key_issues_to_review:",
keys_fix_yaml=["ticket_compliance_check", "estimated_effort_to_review_[1-5]:", "risk_level:", "merge_recommendation:", "review_priority_files:", "security_concerns:", "key_issues_to_review:",
"relevant_file:", "relevant_line:", "suggestion:"],
first_key=first_key, last_key=last_key)
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated
github_action_output(data, 'review')

if 'review' not in data:
get_logger().exception("Failed to parse review data", artifact={"data": data})
return ""
get_logger().info(f"Risk level: {data.get('review', {}).get('risk_level')}")
get_logger().info(f"Merge recommendation: {data.get('review', {}).get('merge_recommendation')}")
get_logger().info(f"Priority files: {data.get('review', {}).get('review_priority_files')}")

# move data['review'] 'key_issues_to_review' key to the end of the dictionary
if 'key_issues_to_review' in data['review']:
Expand Down Expand Up @@ -273,6 +321,7 @@ def _prepare_pr_review(self) -> str:
# Add custom labels from the review prediction (effort, security)
self.set_review_labels(data)


if markdown_text == None or len(markdown_text) == 0:
markdown_text = ""

Expand Down
Loading