<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Niv L.</title>
    <description>The latest articles on DEV Community by Niv L. (@neevo).</description>
    <link>https://dev.to/neevo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4061275%2Ff0b8734b-1868-4476-be99-5ea1e19e6c45.jpg</url>
      <title>DEV Community: Niv L.</title>
      <link>https://dev.to/neevo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/neevo"/>
    <language>en</language>
    <item>
      <title>My AI PR Reviewer Kept Repeating Itself — Here's the Bug and the Fix</title>
      <dc:creator>Niv L.</dc:creator>
      <pubDate>Mon, 03 Aug 2026 21:00:28 +0000</pubDate>
      <link>https://dev.to/neevo/my-ai-pr-reviewer-kept-repeating-itself-heres-the-bug-and-the-fix-1po9</link>
      <guid>https://dev.to/neevo/my-ai-pr-reviewer-kept-repeating-itself-heres-the-bug-and-the-fix-1po9</guid>
      <description>&lt;p&gt;I built &lt;a href="https://github.com/NivL1/ai-pr-reviewer" rel="noopener noreferrer"&gt;ai-pr-reviewer&lt;/a&gt; — a GitHub Action that runs an LLM-powered code review on pull requests, posting inline comments the way a human reviewer would. It worked. Then I started actually using it on my own PRs, and noticed something annoying: every time I pushed a new commit to an open PR, it would re-post comments I'd already seen — including ones I'd already fixed, and one I'd already argued it was wrong about in a reply.&lt;/p&gt;

&lt;p&gt;Not duplicate text. The same substance, reviewed fresh, every single push.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff9scx5xt4e8ft4y7lt7x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff9scx5xt4e8ft4y7lt7x.png" alt="Example inline review comment posted by the bot" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why&lt;/strong&gt;&lt;br&gt;
The orchestration logic was about as simple as you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async reviewPullRequest(req: ReviewRequest): Promise&amp;lt;void&amp;gt; {
  const { owner, repo, prNumber, headSha } = req;

  const rawDiff = await this.github.fetchPullRequestDiff(owner, repo, prNumber);
  const diff = this.filterDiff(rawDiff);
  // ... size guardrail, then hand off to the LLM
  const result = await this.llm.reviewDiff(diff);
  await this.github.postReview(owner, repo, prNumber, headSha, result.summary, result.comments);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;fetchPullRequestDiff always pulls the full base...head diff for the entire PR — not just what changed since the last time it ran. Every push re-triggers this from scratch, with zero memory of anything it said last time. Ten commits into a PR, it's re-reading and re-judging the same nine commits' worth of code it already reviewed, nine times over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: a marker, not a database&lt;/strong&gt;&lt;br&gt;
The obvious fix is "remember what you already reviewed." The less obvious part is where to remember it. I didn't want to add a database just for this — the whole service is intentionally stateless. But GitHub already keeps a full history of every review posted on a PR. So: stamp a hidden marker into every review body, and use GitHub's own API as the source of truth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Hidden in every review body we post, so we can recognize our own past
// reviews on a PR (and find the commit they were posted against) without
// needing a database — GitHub's own review list is the source of truth.
const REVIEW_MARKER = '&amp;lt;!-- ai-pr-reviewer:review --&amp;gt;';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, before reviewing, look back through the PR's review history for our own last review, and pull the commit it was posted against:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async findLastReviewedCommit(owner: string, repo: string, prNumber: number): Promise&amp;lt;string | null&amp;gt; {
  const reviews = await this.octokit.paginate(this.octokit.pulls.listReviews, {
    owner, repo, pull_number: prNumber, per_page: 100,
  });

  const ours = reviews.filter((review) =&amp;gt; review.body?.includes(REVIEW_MARKER));
  if (ours.length === 0) return null;

  // Sort by id (monotonically increasing, assigned at creation) rather
  // than trusting listReviews' response order to stay oldest-first.
  ours.sort((a, b) =&amp;gt; a.id - b.id);
  return ours[ours.length - 1].commit_id ?? null;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that in hand, the orchestrator diffs from there to the new head, instead of from the PR's base:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private async fetchDiff(owner: string, repo: string, prNumber: number, headSha: string): Promise&amp;lt;string&amp;gt; {
  const lastReviewedSha = await this.github.findLastReviewedCommit(owner, repo, prNumber);

  if (!lastReviewedSha) {
    return this.github.fetchPullRequestDiff(owner, repo, prNumber); // first review on this PR
  }
  if (lastReviewedSha === headSha) {
    return ''; // nothing's changed since we last looked
  }

  try {
    return await this.github.fetchDiffSince(owner, repo, lastReviewedSha, headSha);
  } catch {
    return this.github.fetchPullRequestDiff(owner, repo, prNumber); // e.g. old commit unreachable after a force-push
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three states, three behaviors: never reviewed this PR → full diff. Already reviewed this exact commit → do nothing, don't even call the LLM. Reviewed an earlier commit → diff just the new part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix caught a bug in itself&lt;/strong&gt;&lt;br&gt;
Here's the part I didn't expect: this repo dogfoods itself — it reviews its own pull requests. When I opened the PR for this exact change, it reviewed itself and pointed out that ours[ours.length - 1] was trusting listReviews' response order to stay oldest-first, which isn't actually a documented guarantee — sorting by id (as shown above) was the fix, added after the bot flagged it on its own PR. A review-deduplication feature getting correctness-reviewed by the thing it was built to fix felt like a good sign it was working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;br&gt;
Verified end to end: a PR with no changes since the last review now gets skipped entirely — no LLM call, no repeated comments. A PR with new commits gets reviewed on just the delta.&lt;/p&gt;

&lt;p&gt;Full diff and tests: &lt;a href="https://github.com/NivL1/ai-pr-reviewer/pull/9" rel="noopener noreferrer"&gt;PR #9&lt;/a&gt;. If you want to try it: it's on the &lt;a href="https://github.com/marketplace/actions/nivl1-ai-pr-reviewer" rel="noopener noreferrer"&gt;GitHub Marketplace&lt;/a&gt; now, one line in a workflow file.&lt;/p&gt;




&lt;p&gt;I'm Niv, a backend Tech Lead working on NestJS/AI infra projects. &lt;a href="https://github.com/NivL1" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/niv-lusky-005956184/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>github</category>
      <category>typescript</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
