Skip to content

feat(improve): make suggestions heading configurable - #2820

Merged
IsmaelMartinez merged 4 commits into
The-PR-Agent:mainfrom
oleksii-tumanov:feature/configurable-improve-heading
Aug 27, 2026
Merged

feat(improve): make suggestions heading configurable#2820
IsmaelMartinez merged 4 commits into
The-PR-Agent:mainfrom
oleksii-tumanov:feature/configurable-improve-heading

Conversation

@oleksii-tumanov

@oleksii-tumanov oleksii-tumanov commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Related to #2038 (Part 2)

Summary

  • add pr_code_suggestions.suggestions_heading for summary-table /improve output
  • keep persistent history on a stable hidden identity and migrate generated legacy comments in place
  • preserve GitLab incremental anchors and route LocalGit output, including no-suggestions results, to improve.md

For example, suggestions_heading = "Guideline Improvement Suggestions" renders ## Guideline Improvement Suggestions ✨ while continuing to update the same persistent summary.

Testing

  • focused improve, review-identity, GitLab, and LocalGit suites: 261 passed
  • full unit suite: 2480 passed, 1 skipped, 1 xfailed

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Make /improve headings configurable with stable comment identity

✨ Enhancement ⚙️ Configuration changes 📝 Documentation 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Adds configurable visible headings for summary-table /improve output.
• Decouples persistent comment discovery from presentation using stable hidden identities.
• Preserves legacy migration, GitLab incremental anchors, and LocalGit heading consistency.
Diagram

graph TD
  CFG["Improve config"] --> FMT["Header formatter"] --> TOOL["Improve tool"] --> HISTORY["Persistent history"] --> HOSTED["Hosted comments"]
  TOOL --> LOCAL["LocalGit output"]
  ID["Stable identities"] --> HISTORY
  ID --> GITLAB["GitLab anchors"] --> HOSTED
Loading
High-Level Assessment

The chosen approach is appropriate: a stable hidden identity cleanly decouples comment discovery from configurable presentation and follows the repository’s existing review-identity pattern. Matching by visible heading would break after renames, while provider-native comment identifiers would require non-portable state and broader provider-specific changes; guarded legacy-shape migration preserves existing generated threads without adopting human comments.

Files changed (11) +622 / -109

Enhancement (3) +225 / -101
utils.pyAdd suggestion heading and identity utilities +40/-7

Add suggestion heading and identity utilities

• Introduces suggestion header and identity enums plus a shared configurable-heading validator. Adds formatting by Markdown level and generalizes hidden identity insertion for reuse beyond review comments.

pr_agent/algo/utils.py

local_git_provider.pyRender configured heading in LocalGit artifacts +4/-2

Render configured heading in LocalGit artifacts

• Formats the improve.md title from the shared suggestions heading setting at level one. Local artifacts intentionally omit hosted-comment identity markers.

pr_agent/git_providers/local_git_provider.py

pr_code_suggestions.pyDecouple suggestion presentation from persistent identity +181/-92

Decouple suggestion presentation from persistent identity

• Applies the configured heading to summary and no-suggestions output, adds distinct hidden identities, and updates persistent history by stable marker. Safely migrates generated legacy comments in place, tightens commit-marker extraction, and preserves best-effort progress-note cleanup.

pr_agent/tools/pr_code_suggestions.py

Bug fix (1) +16 / -6
gitlab_provider.pyPreserve incremental suggestion anchors across heading changes +16/-6

Preserve incremental suggestion anchors across heading changes

• Uses stable summary and no-suggestions markers as preferred GitLab incremental anchors while retaining inline and legacy-heading fallbacks. This keeps incremental ranges correct after custom heading changes.

pr_agent/git_providers/gitlab_provider.py

Tests (5) +367 / -2
e2e_utils.pyExpect stable identity in improve E2E output +5/-1

Expect stable identity in improve E2E output

• Updates the improve output prefix pattern to include the new summary identity marker before the commit marker and suggestions table.

tests/e2e_tests/e2e_utils.py

test_code_suggestions_comment_identity.pyTest configurable headings and suggestion identities +150/-0

Test configurable headings and suggestion identities

• Covers custom, trimmed, default, and invalid headings; bounded idempotent marker insertion; no-suggestions identities; artifact behavior; and check-run output. Verifies presentation remains separate from hosted persistence identity.

tests/unittest/test_code_suggestions_comment_identity.py

test_gitlab_provider.pyTest stable GitLab suggestion anchor selection +70/-1

Test stable GitLab suggestion anchor selection

• Verifies stable summary identities outrank newer legacy headings and custom-heading no-suggestions results can advance incremental anchors.

tests/unittest/test_gitlab_provider.py

test_local_git_provider.pyTest custom LocalGit improve heading +20/-0

Test custom LocalGit improve heading

• Confirms LocalGit writes the configured level-one heading and excludes hidden hosted-comment identity markers.

tests/unittest/test_local_git_provider.py

test_pr_code_suggestions_core.pyTest persistent suggestion migration and matching +122/-0

Test persistent suggestion migration and matching

• Covers in-place migration of generated legacy comments, precedence of stable markers, and rejection of quoted, late, or human-authored lookalikes. Ensures new comments receive the stable summary identity without duplicating persistent threads.

tests/unittest/test_pr_code_suggestions_core.py

Documentation (1) +12 / -0
improve.mdDocument configurable suggestions headings +12/-0

Document configurable suggestions headings

• Documents the new suggestions_heading setting, rendered output, default value, provider persistence behavior, and LocalGit semantics. Clarifies that inline committable suggestions are unaffected.

docs/docs/tools/improve.md

Other (1) +2 / -0
configuration.tomlAdd default suggestions_heading configuration +2/-0

Add default suggestions_heading configuration

• Adds pr_code_suggestions.suggestions_heading with the existing visible title as its default and notes that identity is tracked separately.

pr_agent/settings/configuration.toml

@qodo-code-review

qodo-code-review Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (2) 📜 Skill insights (0)

Grey Divider


Action required

1. Stale GitLab suggestion anchor ✓ Resolved 🐞 Bug ≡ Correctness
Description
When both formats exist, /improve -i searches all stable markers first and consults legacy
headings only if none exist, rather than selecting the newest matching note across both formats. A
newer legacy suggestions run is therefore ignored in favor of an older stable run, so commits
already covered by the newer run can be included again in the incremental suggestions scope.
Code

pr_agent/git_providers/gitlab_provider.py[R518-520]

+            self.previous_review = self._find_anchor_note(self._SUGGESTIONS_STABLE_ANCHORS)
+            if self.previous_review is None:
+                self.previous_review = self._find_anchor_note(self._SUGGESTIONS_LEGACY_ANCHORS)
Relevance

●●● Strong

Recent accepted GitLab incremental-correctness fixes support addressing stale-anchor selection; no
close rejection precedent exists.

PR-#2389
PR-#2806

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
_find_anchor_note() returns the first matching note from GitLab's newest-first order, but the new
two-pass branch limits the first lookup to stable anchors. Thus a stable match from an older note
prevents the second lookup from ever considering a newer legacy match; the added test explicitly
constructs that ordering and expects the older stable anchor.

pr_agent/git_providers/gitlab_provider.py[478-486]
pr_agent/git_providers/gitlab_provider.py[517-520]
pr_agent/git_providers/gitlab_provider.py[652-665]
tests/unittest/test_gitlab_provider.py[1662-1691]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
GitLab incremental `/improve` anchor selection currently prioritizes stable identities over legacy headers, even when the legacy comment is newer. This creates a stale anchor and reprocesses commits covered by the newer legacy suggestions result.

## Issue Context
`_find_anchor_note()` relies on GitLab notes being newest-first and returns the first matching note. Pass the combined accepted suggestion anchors in one lookup (or otherwise compare timestamps) so selection remains chronological while retaining stable and legacy compatibility.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[478-486]
- pr_agent/git_providers/gitlab_provider.py[517-522]

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



Remediation recommended

2. Gitea persistent updates duplicate 🐞 Bug ☼ Reliability ⭐ New
Description
The new dictionary-aware matcher can select a Gitea comment, but get_comment_url(comment) then
raises because Gitea's implementation uses attribute access on dictionary-shaped comments. The
caught exception falls through to the new-comment path, so repeated persistent /improve runs
publish duplicate threads instead of updating the matched comment.
Code

pr_agent/tools/pr_code_suggestions.py[454]

+                    comment_url = git_provider.get_comment_url(comment)
Relevance

●●● Strong

Recent accepted precedent specifically protects persistent updates from cleanup/fallback exceptions
causing duplicate suggestion threads.

PR-#2404
PR-#2649

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed updater explicitly normalizes dictionary bodies and then unconditionally retrieves a URL
before editing. Gitea documents dictionary-shaped comment payloads and supports them in
edit_comment, while get_comment_url dereferences comment.html_url; the updater catches that
failure and reaches its fallback publication path.

pr_agent/tools/pr_code_suggestions.py[368-372]
pr_agent/tools/pr_code_suggestions.py[452-465]
pr_agent/tools/pr_code_suggestions.py[522-535]
pr_agent/git_providers/gitea_provider.py[307-318]
pr_agent/git_providers/gitea_provider.py[364-371]
pr_agent/git_providers/gitea_provider.py[634-646]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Persistent suggestion updates recognize dictionary-shaped Gitea comments but fail while retrieving their URL, then fall through and publish a duplicate thread.

## Issue Context
Gitea's issue comments are dictionary-shaped and its edit path already supports dictionaries. URL retrieval must either support that shape or remain non-fatal to the actual persistent-comment edit.

## Fix Focus Areas
- pr_agent/tools/pr_code_suggestions.py[452-455]
- pr_agent/git_providers/gitea_provider.py[307-308]
- pr_agent/git_providers/gitea_provider.py[364-371]

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


3. Persistent table slice exceeds limit 📘 Rule violation ⚙ Maintainability ⭐ New
Description
The new table-extraction line exceeds Ruff's configured 120-character maximum. This violates the
required Python line-length rule and should be wrapped across multiple physical lines.
Code

pr_agent/tools/pr_code_suggestions.py[471]

+                                                table_index:prev_suggestions.rfind("</table>") + len("</table>")]
Relevance

●●● Strong

Recent repository precedent accepts Ruff line-length fixes in modified Python code, including
persistent suggestion code.

PR-#2381
PR-#2212

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2694655 requires every changed Python line to remain at or below 120 characters,
while the changed slice expression at line 471 exceeds that limit.

Rule 2694655: Enforce 120-character maximum line length in Python source per Ruff config
pr_agent/tools/pr_code_suggestions.py[470-471]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `prev_suggestion_table` slice exceeds the configured 120-character Python line limit.

## Issue Context
`pyproject.toml` configures Ruff with `line-length = 120`, and this changed physical line is longer than that limit.

## Fix Focus Areas
- pr_agent/tools/pr_code_suggestions.py[470-471]

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


4. REVIEW_START_WITH violates string style 📘 Rule violation ⚙ Maintainability ⭐ New
Description
The modified REVIEW_START_WITH value uses a single-quoted Python string even though double quotes
would require no additional escaping. It also exceeds the configured 120-character line limit.
Code

tests/e2e_tests/e2e_utils.py[4]

+REVIEW_START_WITH = '## PR Reviewer Guide 🔍\n\n<!-- pr-agent:review:full -->\n\n<table>\n<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>:'
Relevance

●● Moderate

Quote-style findings have both recent acceptances and same-day rejections; line length favors
acceptance but team behavior is mixed.

PR-#2796
PR-#2774
PR-#2598

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed line is a single-quoted literal with no double quotes in its value that would justify
the exception, and its physical length is greater than 120 characters.

Rule 2694657: Use double quotes for all Python string literals
Rule 2694655: Enforce 120-character maximum line length in Python source per Ruff config
tests/e2e_tests/e2e_utils.py[4-4]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The changed `REVIEW_START_WITH` assignment uses a single-quoted string and exceeds 120 characters.

## Issue Context
Python strings must use double quotes unless that would avoid significant escaping, and Ruff is configured for a 120-character maximum.

## Fix Focus Areas
- tests/e2e_tests/e2e_utils.py[4-4]

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


View medium (3)
5. Provider class check bypasses capabilities ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
publish_no_suggestions() branches on the concrete LocalGitProvider class instead of querying
provider support. This couples tool behavior to one implementation and violates the required
capability-based provider dispatch.
Code

pr_agent/tools/pr_code_suggestions.py[304]

+            if isinstance(self.git_provider, LocalGitProvider):
Relevance

●●● Strong

Capability-based dispatch is an established accepted pattern, and this violates the
provider-abstraction rule.

PR-#2491
PR-#2389

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2694702 explicitly prohibits conditional behavior based on concrete git-provider types. The
changed branch checks isinstance(self.git_provider, LocalGitProvider) before routing
no-suggestions output, while the provider abstraction already exposes support-query methods.

Rule 2694702: Avoid isinstance checks on git provider classes; use capability queries
pr_agent/tools/pr_code_suggestions.py[304-306]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`publish_no_suggestions()` uses `isinstance(self.git_provider, LocalGitProvider)` to select provider-specific behavior. Replace this concrete-type branch with a capability/support query on the provider.

## Issue Context
PR Compliance ID 2694702 requires git-provider behavior to be selected through capabilities rather than concrete provider classes. Define an explicit capability with a safe default, enable it for LocalGit, use it in the tool, and remove the now-unneeded `LocalGitProvider` import.

## Fix Focus Areas
- pr_agent/tools/pr_code_suggestions.py[304-306]
- pr_agent/git_providers/git_provider.py[117-127]
- pr_agent/git_providers/local_git_provider.py[67-71]

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


6. Local no-suggestions misrouted ✓ Resolved 🐞 Bug ≡ Correctness
Description
When LocalGit produces no suggestions, run() calls publish_no_suggestions(), which now adds a
hidden identity and sends the result through publish_comment(); LocalGit therefore writes it to
review.md instead of the intended improve.md. This also violates the new LocalGit behavior of
omitting hidden identity markers, while the newly updated publish_code_suggestions([])
implementation that correctly writes a marker-free custom heading to improve.md is never reached.
Code

pr_agent/tools/pr_code_suggestions.py[R301-304]

+            pr_body = add_comment_identity(
+                pr_body,
+                PRCodeSuggestionsIdentity.NO_SUGGESTIONS.value,
+            )
Relevance

●●● Strong

Accepted precedents in this tool favor correcting provider-specific output routing and preserving
intended LocalGit publication behavior.

PR-#2404
PR-#2381

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The tool exits into publish_no_suggestions() before any inline/provider-specific suggestion
publishing, and that method uses publish_comment() after inserting the new marker. LocalGit's
publish_comment() writes exclusively to review_path, whereas its dedicated suggestions method
writes marker-free output to improve_path; the provider explicitly defines /improve output as
improve.md.

pr_agent/tools/pr_code_suggestions.py[192-195]
pr_agent/tools/pr_code_suggestions.py[297-314]
pr_agent/git_providers/local_git_provider.py[24-32]
pr_agent/git_providers/local_git_provider.py[47-52]
pr_agent/git_providers/local_git_provider.py[124-131]
pr_agent/git_providers/local_git_provider.py[143-169]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
LocalGit no-suggestions runs currently add a hosted-comment identity marker and publish through `publish_comment()`, which writes to `review.md`; they should write marker-free output to `improve.md`.

## Issue Context
`LocalGitProvider.publish_code_suggestions([])` already formats the configured heading and no-suggestions message correctly. Route the LocalGit no-suggestions branch through that provider method before adding comment identities, while preserving hosted-provider behavior.

## Fix Focus Areas
- pr_agent/tools/pr_code_suggestions.py[297-314]
- pr_agent/git_providers/local_git_provider.py[124-168]
- tests/unittest/test_local_git_provider.py[109-136]

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


7. suggestions_heading missing root config ✓ Resolved 📘 Rule violation ≡ Correctness
Description
The new pr_code_suggestions.suggestions_heading runtime setting is defined only in
pr_agent/settings/configuration.toml, while .pr_agent.toml has no matching key. This leaves the
two required configuration locations out of sync.
Code

pr_agent/settings/configuration.toml[173]

+suggestions_heading = "PR Code Suggestions"
Relevance

●●● Strong

Recent accepted precedents require configuration defaults and related settings to remain
synchronized across configuration files.

PR-#2490
PR-#2231

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires changed configuration keys to exist consistently in both the root
.pr_agent.toml and the corresponding pr_agent/settings/*.toml file. The PR adds the setting at
pr_agent/settings/configuration.toml[173-173]; .pr_agent.toml[1-27] contains no corresponding
section or key, and pr_agent/algo/utils.py[115-117] confirms the key is read at runtime.

Rule 2694685: Keep .pr_agent.toml and pr_agent/settings/*.toml configuration in sync on behavior changes
pr_agent/settings/configuration.toml[172-173]
.pr_agent.toml[1-27]
pr_agent/algo/utils.py[115-117]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `pr_code_suggestions.suggestions_heading` key exists in `pr_agent/settings/configuration.toml` but is absent from the root `.pr_agent.toml`.

## Issue Context
PR Compliance ID 2694685 requires configuration keys and defaults to remain synchronized between these locations when behavior changes.

## Fix Focus Areas
- pr_agent/settings/configuration.toml[172-173]
- .pr_agent.toml[1-27]

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


  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.

Grey Divider

Context sources
✅ Compliance rules (platform): 34 rules
Review mode: 🧠 Deep: The latest push adds substantial, independent behavior across persistent comment migration, GitLab incremental anchoring, LocalGit artifact routing, and lifecycle/concurrency cleanup, creating a bug-dense patch where redundant review passes are materially valuable.

Grey Divider

Tip of the day
💡 Did you know, you can ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 554bc16

Results up to commit 8bfa915 🧠 Deep


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 📜 Skill insights (0)


Remediation recommended
1. suggestions_heading missing root config ✓ Resolved 📘 Rule violation ≡ Correctness
Description
The new pr_code_suggestions.suggestions_heading runtime setting is defined only in
pr_agent/settings/configuration.toml, while .pr_agent.toml has no matching key. This leaves the
two required configuration locations out of sync.
Code

pr_agent/settings/configuration.toml[173]

+suggestions_heading = "PR Code Suggestions"
Relevance

●●● Strong

Recent accepted precedents require configuration defaults and related settings to remain
synchronized across configuration files.

PR-#2490
PR-#2231

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires changed configuration keys to exist consistently in both the root
.pr_agent.toml and the corresponding pr_agent/settings/*.toml file. The PR adds the setting at
pr_agent/settings/configuration.toml[173-173]; .pr_agent.toml[1-27] contains no corresponding
section or key, and pr_agent/algo/utils.py[115-117] confirms the key is read at runtime.

Rule 2694685: Keep .pr_agent.toml and pr_agent/settings/*.toml configuration in sync on behavior changes
pr_agent/settings/configuration.toml[172-173]
.pr_agent.toml[1-27]
pr_agent/algo/utils.py[115-117]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `pr_code_suggestions.suggestions_heading` key exists in `pr_agent/settings/configuration.toml` but is absent from the root `.pr_agent.toml`.

## Issue Context
PR Compliance ID 2694685 requires configuration keys and defaults to remain synchronized between these locations when behavior changes.

## Fix Focus Areas
- pr_agent/settings/configuration.toml[172-173]
- .pr_agent.toml[1-27]

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


2. Local no-suggestions misrouted ✓ Resolved 🐞 Bug ≡ Correctness
Description
When LocalGit produces no suggestions, run() calls publish_no_suggestions(), which now adds a
hidden identity and sends the result through publish_comment(); LocalGit therefore writes it to
review.md instead of the intended improve.md. This also violates the new LocalGit behavior of
omitting hidden identity markers, while the newly updated publish_code_suggestions([])
implementation that correctly writes a marker-free custom heading to improve.md is never reached.
Code

pr_agent/tools/pr_code_suggestions.py[R301-304]

+            pr_body = add_comment_identity(
+                pr_body,
+                PRCodeSuggestionsIdentity.NO_SUGGESTIONS.value,
+            )
Relevance

●●● Strong

Accepted precedents in this tool favor correcting provider-specific output routing and preserving
intended LocalGit publication behavior.

PR-#2404
PR-#2381

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The tool exits into publish_no_suggestions() before any inline/provider-specific suggestion
publishing, and that method uses publish_comment() after inserting the new marker. LocalGit's
publish_comment() writes exclusively to review_path, whereas its dedicated suggestions method
writes marker-free output to improve_path; the provider explicitly defines /improve output as
improve.md.

pr_agent/tools/pr_code_suggestions.py[192-195]
pr_agent/tools/pr_code_suggestions.py[297-314]
pr_agent/git_providers/local_git_provider.py[24-32]
pr_agent/git_providers/local_git_provider.py[47-52]
pr_agent/git_providers/local_git_provider.py[124-131]
pr_agent/git_providers/local_git_provider.py[143-169]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
LocalGit no-suggestions runs currently add a hosted-comment identity marker and publish through `publish_comment()`, which writes to `review.md`; they should write marker-free output to `improve.md`.

## Issue Context
`LocalGitProvider.publish_code_suggestions([])` already formats the configured heading and no-suggestions message correctly. Route the LocalGit no-suggestions branch through that provider method before adding comment identities, while preserving hosted-provider behavior.

## Fix Focus Areas
- pr_agent/tools/pr_code_suggestions.py[297-314]
- pr_agent/git_providers/local_git_provider.py[124-168]
- tests/unittest/test_local_git_provider.py[109-136]

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


Results up to commit 0167e09 🧠 Deep


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 📜 Skill insights (0)


Action required
1. Stale GitLab suggestion anchor ✓ Resolved 🐞 Bug ≡ Correctness
Description
When both formats exist, /improve -i searches all stable markers first and consults legacy
headings only if none exist, rather than selecting the newest matching note across both formats. A
newer legacy suggestions run is therefore ignored in favor of an older stable run, so commits
already covered by the newer run can be included again in the incremental suggestions scope.
Code

pr_agent/git_providers/gitlab_provider.py[R518-520]

+            self.previous_review = self._find_anchor_note(self._SUGGESTIONS_STABLE_ANCHORS)
+            if self.previous_review is None:
+                self.previous_review = self._find_anchor_note(self._SUGGESTIONS_LEGACY_ANCHORS)
Relevance

●●● Strong

Recent accepted GitLab incremental-correctness fixes support addressing stale-anchor selection; no
close rejection precedent exists.

PR-#2389
PR-#2806

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
_find_anchor_note() returns the first matching note from GitLab's newest-first order, but the new
two-pass branch limits the first lookup to stable anchors. Thus a stable match from an older note
prevents the second lookup from ever considering a newer legacy match; the added test explicitly
constructs that ordering and expects the older stable anchor.

pr_agent/git_providers/gitlab_provider.py[478-486]
pr_agent/git_providers/gitlab_provider.py[517-520]
pr_agent/git_providers/gitlab_provider.py[652-665]
tests/unittest/test_gitlab_provider.py[1662-1691]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
GitLab incremental `/improve` anchor selection currently prioritizes stable identities over legacy headers, even when the legacy comment is newer. This creates a stale anchor and reprocesses commits covered by the newer legacy suggestions result.

## Issue Context
`_find_anchor_note()` relies on GitLab notes being newest-first and returns the first matching note. Pass the combined accepted suggestion anchors in one lookup (or otherwise compare timestamps) so selection remains chronological while retaining stable and legacy compatibility.

## Fix Focus Areas
- pr_agent/git_providers/gitlab_provider.py[478-486]
- pr_agent/git_providers/gitlab_provider.py[517-522]

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



Remediation recommended
2. Provider class check bypasses capabilities ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
publish_no_suggestions() branches on the concrete LocalGitProvider class instead of querying
provider support. This couples tool behavior to one implementation and violates the required
capability-based provider dispatch.
Code

pr_agent/tools/pr_code_suggestions.py[304]

+            if isinstance(self.git_provider, LocalGitProvider):
Relevance

●●● Strong

Capability-based dispatch is an established accepted pattern, and this violates the
provider-abstraction rule.

PR-#2491
PR-#2389

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2694702 explicitly prohibits conditional behavior based on concrete git-provider types. The
changed branch checks isinstance(self.git_provider, LocalGitProvider) before routing
no-suggestions output, while the provider abstraction already exposes support-query methods.

Rule 2694702: Avoid isinstance checks on git provider classes; use capability queries
pr_agent/tools/pr_code_suggestions.py[304-306]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`publish_no_suggestions()` uses `isinstance(self.git_provider, LocalGitProvider)` to select provider-specific behavior. Replace this concrete-type branch with a capability/support query on the provider.

## Issue Context
PR Compliance ID 2694702 requires git-provider behavior to be selected through capabilities rather than concrete provider classes. Define an explicit capability with a safe default, enable it for LocalGit, use it in the tool, and remove the now-unneeded `LocalGitProvider` import.

## Fix Focus Areas
- pr_agent/tools/pr_code_suggestions.py[304-306]
- pr_agent/git_providers/git_provider.py[117-127]
- pr_agent/git_providers/local_git_provider.py[67-71]

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


Grey Divider

Qodo Logo

@github-actions github-actions Bot added the feature 💡 label Aug 26, 2026
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 23dc87d

Comment thread pr_agent/git_providers/gitlab_provider.py Outdated
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 0167e09

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 1272b46

@IsmaelMartinez IsmaelMartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving, with small things inline.

Picking the anchor by anchor_time across the combined tuple is better than the fix I was going to suggest. Yours also handles a persistent comment that was edited more recently than a newer note was created. The capability method is the right shape for the LocalGit branch too.

One thing to drop before merge: the three .pr_agent.toml lines. That file's own header says it feeds Qodo's hosted agent, not the pr-agent in this repo, and the value is the same as the default anyway. #2823 needs the same three lines removed, and it still conflicts with this PR in algo/utils.py.

Thanks for building the hidden anchor from the issue rather than a plain config key, and for turning both bot findings round this fast.

Comment thread .pr_agent.toml Outdated
Comment on lines +10 to +12
[pr_code_suggestions]
suggestions_heading = "PR Code Suggestions"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This file's header says it is read by Qodo's hosted agent, not by the pr-agent in this repo, so the rule that asked for the sync does not apply here. The value is an exact copy of the default in configuration.toml as well. #2797 left it alone for the same reason. Worth dropping the same three lines from #2823.

Suggested change
[pr_code_suggestions]
suggestions_heading = "PR Code Suggestions"

Comment thread tests/e2e_tests/e2e_utils.py Outdated
@@ -2,7 +2,11 @@

PR_HEADER_START_WITH = '### **User description**\nupdate cli_pip.py\n\n\n___\n\n### **PR Type**'
REVIEW_START_WITH = '## PR Reviewer Guide 🔍\n\n<table>\n<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>:'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

While you are here: part 1 added the review identity marker but not the matching prefix here, so this constant no longer matches a published review. Nothing catches it, because e2e_tests.yaml only runs on workflow_dispatch.

Suggested change
REVIEW_START_WITH = '## PR Reviewer Guide 🔍\n\n<table>\n<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>:'
REVIEW_START_WITH = '## PR Reviewer Guide 🔍\n\n<!-- pr-agent:review:full -->\n\n<table>\n<tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>:'

)
else:
# get the text of the previous suggestions until the latest commit
sections = prev_suggestions.split(history_header.strip())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nothing in tests/unittest reaches this branch. Coverage says lines 487-512 are never executed by the file's 45 tests, yet this is the normal state from the second /improve onward.

I ran it by hand and the migration is correct, so this is a gap rather than a bug. I have put a test for it in a suggestion at the end of the test file.


provider.edit_comment.assert_not_called()
published = provider.publish_comment.call_args.args[0]
assert PRCodeSuggestionsIdentity.SUMMARY.value in published

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A test for the branch above, if you want it. It builds a persistent comment that already has a #### Previous suggestions section and checks the custom heading and the marker survive, and that both the old and the newly folded entry are kept.

I ran it against your branch merged onto today's main: 46 pass. With it, the uncovered range at 487-512 drops to a single line, 501, which is the max_previous_comments eviction and wants its own case.

Suggested change
assert PRCodeSuggestionsIdentity.SUMMARY.value in published
assert PRCodeSuggestionsIdentity.SUMMARY.value in published
def test_custom_heading_is_kept_when_a_history_section_already_exists():
existing = MagicMock()
existing.body = (
"## Previous Custom Heading ✨\n\n"
f"{PRCodeSuggestionsIdentity.SUMMARY.value}\n\n"
"<!-- aaa1111 -->\n\n"
"Latest suggestions up to commit aaa1111\n\n"
"<table>latest</table>\n\n___\n\n"
"#### Previous suggestions\n"
"<details><summary>Suggestions up to commit 0000000</summary>\n"
"<br><table>older</table>\n\n</details>\n"
)
provider = _persistent_provider([existing])
custom_header = "## Latest Custom Heading ✨"
result = PRCodeSuggestions.publish_persistent_comment_with_history(
provider,
f"{custom_header}\n\n<table>new suggestions</table>",
initial_header=custom_header,
name="suggestions",
identity_marker=PRCodeSuggestionsIdentity.SUMMARY.value,
legacy_initial_header=PRCodeSuggestionsHeader.SUMMARY.value,
)
assert result is existing
updated = provider.edit_comment.call_args.args[1]
assert updated.startswith(
f"{custom_header}\n\n{PRCodeSuggestionsIdentity.SUMMARY.value}\n\n<!-- deadbee -->"
)
assert "Suggestions up to commit aaa1111" in updated
assert "Suggestions up to commit 0000000" in updated
provider.publish_comment.assert_not_called()

@oleksii-tumanov
oleksii-tumanov force-pushed the feature/configurable-improve-heading branch from 1272b46 to 554bc16 Compare August 27, 2026 15:06
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 554bc16

@IsmaelMartinez IsmaelMartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving on the rebase. All three taken, and thanks for adding the test rather than just noting it.

Suite is green on your head merged onto today's main. Ready from my side.

Only thing left is ordering against #2823, which touches the same two files. No action needed unless it lands first.

@IsmaelMartinez
IsmaelMartinez merged commit d2f917a into The-PR-Agent:main Aug 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants