<?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: Igor Pavlenko</title>
    <description>The latest articles on DEV Community by Igor Pavlenko (@marround).</description>
    <link>https://dev.to/marround</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%2F4101215%2Fc44cf48e-8941-4358-aa0d-79288dabde3a.png</url>
      <title>DEV Community: Igor Pavlenko</title>
      <link>https://dev.to/marround</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marround"/>
    <language>en</language>
    <item>
      <title>AI Code Review as a Quality Gate, Not a Chat: Pre-commit, Pre-push, PR</title>
      <dc:creator>Igor Pavlenko</dc:creator>
      <pubDate>Sun, 30 Aug 2026 10:03:54 +0000</pubDate>
      <link>https://dev.to/marround/ai-code-review-as-a-quality-gate-not-a-chat-pre-commit-pre-push-pr-36ld</link>
      <guid>https://dev.to/marround/ai-code-review-as-a-quality-gate-not-a-chat-pre-commit-pre-push-pr-36ld</guid>
      <description>&lt;p&gt;Today, in most teams, the path code takes to production looks roughly like this:&lt;/p&gt;

&lt;p&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%2Fwrut333hxehttsi2m9t3.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%2Fwrut333hxehttsi2m9t3.png" alt="The path code takes to production today" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Linters and tests became gates a long time ago. Nobody argues with them: they stand on the road and refuse to let through what they don't like. AI review is being wired in everywhere now - into GitHub, into GitLab, as standalone bots. But in the role I keep running into, it stands in exactly one place: &lt;strong&gt;one more commenter on the pull request&lt;/strong&gt;. I want to talk about a different role - &lt;strong&gt;one more gate&lt;/strong&gt;, standing next to the linter and the tests rather than next to the reviewer. And about the properties a probabilistic check must have before you can let it into the pipeline alongside deterministic ones.&lt;/p&gt;

&lt;p&gt;Here is the whole thing at once; the rest of the article walks through each step:&lt;/p&gt;

&lt;p&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%2Fz90779bepyqq4wn7vcup.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%2Fz90779bepyqq4wn7vcup.png" alt="The same path, with AI review as a gate" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Conflict of interest, stated up front: I'll be showing all of this on my own tool. ReviewGate is a &lt;code&gt;reviewgate&lt;/code&gt; binary for your terminal, your git hooks and your agent, plus a bot for merge and pull requests. It is free and &lt;strong&gt;closed source&lt;/strong&gt;: there are no sources to read, it lives on donations, there is no account and no sign-up, and you pay for the model with your own key. "Self-hosted" here does not mean "open source" - better to learn that now than after you install it. (I plan to open it later. Not today.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline today, and the hole in it
&lt;/h2&gt;

&lt;p&gt;Take a small Angular project: an internal request panel that support managers keep open all day without closing the tab. The task of the week is search over requests - a search box, a status filter, a new service. Four files, about 140 lines. Lint is green, the build is green, search works in the browser. A client would sign it off.&lt;/p&gt;

&lt;p&gt;The project with its full history lives on GitHub: &lt;a href="https://github.com/ReviewGate/service-desk" rel="noopener noreferrer"&gt;ReviewGate/service-desk&lt;/a&gt;. The "before review" state is the tag &lt;code&gt;search-unreviewed&lt;/code&gt;, and every snippet below comes from there.&lt;/p&gt;

&lt;p&gt;Here is the method that fires on every keystroke in the search box (and on every filter change), verbatim from &lt;a href="https://github.com/ReviewGate/service-desk/blob/search-unreviewed/src/app/features/requests/request-list.component.ts#L71-L87" rel="noopener noreferrer"&gt;&lt;code&gt;request-list.component.ts&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;runSearch&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statusFilter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statusFilter&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foundAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;([]),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What catches this today? Tests - if there were any - would pass: search finds things. The linter says nothing: there is nothing wrong with the line &lt;code&gt;subscribe({…})&lt;/code&gt;, it is a legal call, and any Angular project has hundreds of them. What is wrong here is something else, and you can only see it by reading the whole file and the template next to it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;higher up, &lt;a href="https://github.com/ReviewGate/service-desk/blob/search-unreviewed/src/app/features/requests/request-list.component.ts#L42-L53" rel="noopener noreferrer"&gt;in the constructor of &lt;strong&gt;the same file&lt;/strong&gt;&lt;/a&gt;, it is done properly - &lt;code&gt;.pipe(takeUntilDestroyed())&lt;/code&gt;. So the author knows how it should be done. This is a slip, not ignorance;&lt;/li&gt;
&lt;li&gt;the method is called on &lt;strong&gt;every keystroke&lt;/strong&gt; - there isn't one subscription, there are as many as the manager typed letters, and the tab stays alive for days;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;error&lt;/code&gt; branch quietly turns a server failure into an empty result - the manager cannot tell a broken search from "nothing found".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is how those two findings look in the report, verbatim from one run (I only trimmed the layout):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔴 &lt;strong&gt;critical&lt;/strong&gt; · &lt;code&gt;request-list.component.ts:78&lt;/code&gt; · rule &lt;code&gt;team:no-leaking-subscriptions&lt;/code&gt;&lt;br&gt;
subscribe() in runSearch() is not tied to the component lifecycle: if the manager leaves the screen before the response arrives, the callback still runs and writes into signals of a destroyed component. Team rule no-leaking-subscriptions requires takeUntilDestroyed() (or async pipe/toSignal) even for finite streams.&lt;br&gt;
Judge: &lt;em&gt;confirmed&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;🟠 &lt;strong&gt;major&lt;/strong&gt; · &lt;code&gt;request-list.component.ts:85&lt;/code&gt; · rule &lt;code&gt;team:three-states-on-load&lt;/code&gt;&lt;br&gt;
When the search request fails, error() sets found([]), which the template renders as 'No requests yet.' / 'Found: 0'. The manager cannot tell an error from an empty result, and there is no 'searching…' state.&lt;br&gt;
Judge: &lt;em&gt;confirmed&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Five severity levels, one scale across the whole tool - in findings, in gate thresholds and in your team's own rules:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;level in the config&lt;/th&gt;
&lt;th&gt;what it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;⛔&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blocker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;unconditional block: a secret or credentials in the diff, irreversible data loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔴&lt;/td&gt;
&lt;td&gt;&lt;code&gt;critical&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;security, data loss, a crash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟠&lt;/td&gt;
&lt;td&gt;&lt;code&gt;major&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a probable bug, an unhandled error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔵&lt;/td&gt;
&lt;td&gt;&lt;code&gt;minor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a small thing, a maintainability risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⚪&lt;/td&gt;
&lt;td&gt;&lt;code&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an observation, no action needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gate threshold is written in those same words: &lt;code&gt;--fail-on major&lt;/code&gt; means "block on major and above".&lt;/p&gt;

&lt;p&gt;Note the &lt;code&gt;team:&lt;/code&gt; prefix in front of the rule id. That is not "the model knows Angular". That is a rule of &lt;strong&gt;my&lt;/strong&gt; team, written down in a file in the repository - and the finding shows which one. In the terminal and in &lt;code&gt;--json&lt;/code&gt; it is the &lt;code&gt;team:&lt;/code&gt; prefix; in the bot's comments on a pull request the same ownership shows up as a 📐 badge next to the id. You'll see it in the chapter about pull requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Linters don't hallucinate"
&lt;/h3&gt;

&lt;p&gt;True - and they don't see what AI review sees. And AI review does see it, and can be wrong about it. Everything else rests on that symmetry, so let me spell it out on the same diff. Six team-rule violations are planted in it, and the engine brought one more finding of its own, without a &lt;code&gt;team:&lt;/code&gt; prefix. Here is which of those seven a linter catches and which it doesn't:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;finding&lt;/th&gt;
&lt;th&gt;caught by a linter?&lt;/th&gt;
&lt;th&gt;why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;three-states-on-load&lt;/code&gt; - an error rendered as an empty list&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;this is behavior, not syntax: you have to understand that the error branch renders "empty"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;debounce-user-input&lt;/code&gt; - a request per keystroke&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;you have to connect the input event in the template → the handler → the HTTP call; an AST rule doesn't stitch three files together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;an unencoded &lt;code&gt;query&lt;/code&gt; in the request URL (the one without &lt;code&gt;team:&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;the semantics of the data, not the shape of a string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;format-through-pipes&lt;/code&gt; - a time glued from strings, showing &lt;code&gt;14:5&lt;/code&gt; at 14:05&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;a linter doesn't know this is a date rather than a string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;no-leaking-subscriptions&lt;/code&gt; - that very &lt;code&gt;subscribe()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;partly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;the "subscribe without takeUntil" pattern is caught by &lt;code&gt;eslint-plugin-rxjs-angular&lt;/code&gt;; it won't see that the method fires on every keystroke (the part that makes the finding critical) or that the constructor above does it correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;http-only-in-api-service&lt;/code&gt; - a new service injecting &lt;code&gt;HttpClient&lt;/code&gt; around &lt;code&gt;ApiService&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;no-restricted-imports&lt;/code&gt;, and let the linter do it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;signals-in-new-code&lt;/code&gt; - a new &lt;code&gt;BehaviorSubject&lt;/code&gt; instead of a signal&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;strict lint-staged on new files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two out of seven are the linter's job, and the linter should do them: it is free, instant and deterministic. Which brings us to the main point of this whole article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI review belongs next to deterministic checks, not instead of them, and it belongs after them: on every step, first what is free and deterministic, then what costs money and can be wrong.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The contract for a probabilistic gate
&lt;/h2&gt;

&lt;p&gt;If a check can be wrong, you cannot put it into the pipeline on the same terms as a linter. It needs different terms, and these are &lt;strong&gt;requirements&lt;/strong&gt;, not &lt;strong&gt;caveats&lt;/strong&gt;: drop any one of them and your "AI gate" turns into either a wall or noise, and the team will have every reason to switch it off. Here they are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Rules come from the repository, not from "best practices" in general.&lt;/strong&gt; A finding carries a rule id, and the id tells you who is complaining: your team (&lt;code&gt;team:&lt;/code&gt;) or the engine - the stack preset and the shared core. You argue with those two differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. It talks to the pipeline in exit codes, not prose.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt; - the threshold was not exceeded (and with the gate off, always);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2&lt;/code&gt; - the threshold was exceeded;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1&lt;/code&gt; - the run itself failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only &lt;code&gt;2&lt;/code&gt; blocks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It knows how to stay silent.&lt;/strong&gt; A single model call is a lottery: the model will confidently say &lt;em&gt;something&lt;/em&gt;, and you will be the one reading it. So it isn't a call, it is a ladder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;generator&lt;/strong&gt; looks for problems;&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;judge&lt;/strong&gt; (up to two of them) checks every finding with &lt;em&gt;refute&lt;/em&gt; as the default stance - not "rate this" but "prove it isn't true" - and looks not at the diff but at whole files and their neighbors by import;&lt;/li&gt;
&lt;li&gt;if the judges disagree, an &lt;strong&gt;arbiter&lt;/strong&gt; steps in, and only on the disputed ones; with no arbiter configured, a tie is resolved in favor of silence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yes, some findings die along the way. That is a deliberate price: what got through did not get through by accident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. It doesn't block out of the box, and turning blocking on shouldn't be day one.&lt;/strong&gt; The gate is off in the default policy (&lt;code&gt;severity_gate: off&lt;/code&gt;); strictness is something the team turns on explicitly - with a threshold in the config or a flag in a hook. But first comes calibration on your own pull requests, with no power to block:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;watch, over a stretch of real work, what the review finds and how much of it you consider noise;&lt;/li&gt;
&lt;li&gt;fix your rules and your list of deliberate trade-offs (more on that below);&lt;/li&gt;
&lt;li&gt;only then move &lt;code&gt;severity_gate&lt;/code&gt; to a blocking value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A gate switched on before calibration gets switched off on the first false block - and never comes back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. When it breaks, it lets you through - and says so.&lt;/strong&gt; No network, a revoked key, a model outage: none of that is a blocked push.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a broken reviewer must not block a developer in their own branch, hence exit code &lt;code&gt;1&lt;/code&gt;, "allowed unchecked";&lt;/li&gt;
&lt;li&gt;but staying quiet about the skip is not allowed either - a line on stderr is mandatory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;6. You can see what ran and what it cost.&lt;/strong&gt; Under the report there is a "🔬 Run diagnostics" block, switched on with one line in the config (&lt;code&gt;diagnostics: true&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which model ran in which role, and for how many seconds;&lt;/li&gt;
&lt;li&gt;how many candidates the judge dropped, and why;&lt;/li&gt;
&lt;li&gt;the cost of the run right next to it (&lt;code&gt;cost.show&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not debugging, it is trust: a gate you cannot inspect is not a gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. The code never reaches the author of the tool&lt;/strong&gt; - only where you sent it yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;to your model, with your key;&lt;/li&gt;
&lt;li&gt;in a closed network, to a model on your own hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Diffs and code are neither logged nor stored.&lt;/p&gt;

&lt;p&gt;Here is what that looks like in practice - the 🔬 block under two runs of the very same diff, one fast and one full (in the call list the judge is labeled "validator", that's the same thing):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**🔬 Run diagnostics**

**Model context**: diff: 4 files · full files: 10 (12K chars) · environment: ✓ · from team standards (8 rules): 6 of 7 findings

**Calls**:

- generator `deepseek-v4-flash` — 10.8K→19.6K tokens · 173 s · findings: 7

_No judging took place: fast mode — a single generator, no judge._
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**🔬 Run diagnostics**

**Model context**: diff: 4 files · full files: 10 (12K chars) · environment: ✓ · from team standards (8 rules): 6 of 8 findings

**Calls**:

- generator `deepseek-v4-flash` — 10.8K→12.6K tokens (cache: 10.8K) · 105 s · findings: 8
- validator `deepseek-v4-pro` — 10.7K→5.6K tokens · 114 s · dropped: 0 · downgraded: 0

**The judge** confirmed every finding (dropped 0, downgraded 0).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two honest observations from those blocks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The spread is real.&lt;/strong&gt; Same diff, same generator - and it brought 7 findings in one run and 8 in the other. A table of "what's planted in the diff" is a reference point, not a promise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The judge takes away more than findings.&lt;/strong&gt; It can also strip the &lt;em&gt;suggested fix&lt;/em&gt; off a finding it otherwise confirms - when the fix wouldn't compile as written, for instance because of a missing import. The "apply" button in a pull request only shows up on fixes the judge confirmed as mechanical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have no public accuracy metric to offer. Any "N % false positives" I printed here would be a number from one diff on one day. What I can show instead is the machinery that produces those percentages, and it is in the 🔬 block above every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules: from notes to config
&lt;/h2&gt;

&lt;p&gt;Rules are the things you already agreed on and forget by month three. They live wherever they landed: in Confluence, in a pinned chat message, in an ADR, in the team lead's head; at best, in a &lt;code&gt;.md&lt;/code&gt; next to the code. Mine are plain notes in &lt;a href="https://github.com/ReviewGate/service-desk/blob/main/docs/conventions.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/conventions.md&lt;/code&gt;&lt;/a&gt;. Wherever they come from, the move into a config looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;reviewgate rules      &lt;span class="c"&gt;# .reviewgate/config.yml not found at HEAD — default config&lt;/span&gt;
reviewgate init       &lt;span class="c"&gt;# creates BOTH configs: the repo skeleton and your personal one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; detects the stack from the manifests (&lt;code&gt;preset: angular&lt;/code&gt; is already in the file) and never touches what exists - run it twice and it says &lt;code&gt;already exists — left untouched&lt;/code&gt;. The rules go into the repo skeleton. Here is the file from the sandbox, three rules out of eight, everything else as it is (the whole thing: &lt;a href="https://github.com/ReviewGate/service-desk/blob/search-unreviewed/.reviewgate/config.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.reviewgate/config.yml&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Project rules for automated review — the machine-readable version of docs/conventions.md.&lt;/span&gt;
&lt;span class="c1"&gt;# Change a rule there — change it here too, in one commit: otherwise the docs and the policy drift apart.&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
&lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;angular&lt;/span&gt;

&lt;span class="c1"&gt;# The gate is off: I'm the only one here and I decide myself what blocks a push and what doesn't.&lt;/span&gt;
&lt;span class="c1"&gt;# For a particular run I set the threshold with the --fail-on flag when I need it (the pre-push hook).&lt;/span&gt;
&lt;span class="na"&gt;severity_gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;off&lt;/span&gt;

&lt;span class="c1"&gt;# There are no tests in this project and none are planned — don't flag their absence as a problem.&lt;/span&gt;
&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;optional&lt;/span&gt;

&lt;span class="na"&gt;llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generators&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deepseek-v4-flash&lt;/span&gt; &lt;span class="c1"&gt;# hunts: fast and cheap&lt;/span&gt;
  &lt;span class="na"&gt;judges&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deepseek-v4-pro&lt;/span&gt;   &lt;span class="c1"&gt;# judges on full files: stronger and more thoughtful&lt;/span&gt;

  &lt;span class="na"&gt;full_file_context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;      &lt;span class="c1"&gt;# the generator gets whole files for findings, not just the diff&lt;/span&gt;
  &lt;span class="na"&gt;environment_context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;    &lt;span class="c1"&gt;# stack versions into the prompt: Angular 21, zoneless, new control flow&lt;/span&gt;

&lt;span class="c1"&gt;# A collapsed block with timings and the judge's decisions. No code in it, only metadata.&lt;/span&gt;
&lt;span class="na"&gt;diagnostics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# Unambiguous mechanical fixes — as a native suggestion block.&lt;/span&gt;
&lt;span class="na"&gt;committable_suggestions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;cost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;
  &lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;deepseek-v4-flash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;0.44&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1.32&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;deepseek-v4-pro&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1.32&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;3.96&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Rules = only what ESLint can't see. The id ends up in the finding's ruleId.&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;no-leaking-subscriptions&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Every&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;subscription&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;component&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dies&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;together&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;it.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;In&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;order&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;preference:&lt;/span&gt;
      &lt;span class="s"&gt;async&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pipe&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;template,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;toSignal,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;takeUntilDestroyed().&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;bare&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;subscribe()&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;lifecycle&lt;/span&gt;
      &lt;span class="s"&gt;management&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;leak,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;even&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;if&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;looks&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;finite."&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critical&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;three-states-on-load&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Data&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;loading&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;handles&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;three&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;states:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;loading,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;error,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;empty&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;result.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;
      &lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;defect,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;an&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;unfinished&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;bit:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stares&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;an&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;eternal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;“Loading…”."&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;major&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;format-through-pipes&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dates&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;phone&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;numbers&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;are&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;formatted&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pipes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(DatePipe,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sdPhone),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;gluing&lt;/span&gt;
      &lt;span class="s"&gt;strings&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;together&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;component."&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minor&lt;/span&gt;
  &lt;span class="c1"&gt;# … five more rules in the file&lt;/span&gt;

&lt;span class="c1"&gt;# Deliberate trade-offs — don't flag them, the whole class of false positives goes quiet.&lt;/span&gt;
&lt;span class="na"&gt;dont_flag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Localization:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;panel&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;internal,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;UI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;strings&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;right&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;templates&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;that's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;decision,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;i18n&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;isn't&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;planned"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;unit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tests&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deliberately&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;has&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;none"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Inline&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;templates&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;styles&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;small&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;presentational&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;components&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(status-badge)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;that's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;purpose"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;core/api.service.ts&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;legacy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;code,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;move&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;signals&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;happening&lt;/span&gt;
     &lt;span class="s"&gt;gradually.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exception&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;about&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;THIS&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;file:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;still&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;violation"&lt;/span&gt;

&lt;span class="na"&gt;ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.spec.ts"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/assets/**"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.svg"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.png"&lt;/span&gt;

&lt;span class="na"&gt;review_prompt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;extend&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;The panel is internal, managers keep it open all working day without closing the tab.&lt;/span&gt;
    &lt;span class="s"&gt;So on top of that, look at:&lt;/span&gt;
    &lt;span class="s"&gt;- Long-lived subscriptions, timers and intervals: the tab stays alive for days.&lt;/span&gt;
    &lt;span class="s"&gt;- How the screen behaves when the API returns an error or an empty list.&lt;/span&gt;
    &lt;span class="s"&gt;- Actions that change data: what the manager sees if the request didn't go through.&lt;/span&gt;
    &lt;span class="s"&gt;Leave pure styling and anything the linter catches alone.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things make this file worth having.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only what the linter cannot see goes into the rules.&lt;/strong&gt; The comment above &lt;code&gt;rules:&lt;/code&gt; is not decoration: the rest is the linter's job, and the linter should do it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dont_flag&lt;/code&gt; holds deliberate trade-offs.&lt;/strong&gt; Missing tests and inline templates will not be commented on. That silences an entire class of false positives, not a single finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The config is code.&lt;/strong&gt; It lives in the repository, gets committed, travels into the review with the branch, and changes in the same pull request as the agreement it describes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check it without a model and without spending anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;reviewgate rules
&lt;span class="go"&gt;Config loaded: preset=angular, rules=8, gate=off
Stack preset: angular

Team rules:
- team:http-only-in-api-service (critical): Network calls live only in core/api.service.ts. …
- team:no-leaking-subscriptions (critical): Every subscription in a component dies together with it. …
- team:authorization-is-server-side (critical): A button or a menu item hidden by role is a UI convenience, not protection. …
- team:debounce-user-input (major): …
- team:three-states-on-load (major): …
- team:signals-in-new-code (major): …
- team:format-through-pipes (minor): …
- team:routes-from-constants (minor): …

Review guideline:
The panel is internal, managers keep it open all working day without closing the tab. …
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is exactly what the model will see: eight rules with ids and levels, the stack, and the text of &lt;code&gt;review_prompt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This list has a second use, as a lookup table for "where did this finding come from": &lt;strong&gt;if a rule is not on it, the finding did not come from your team&lt;/strong&gt; but from the stack preset or the core, and you argue with it differently. Sonar and your linter are neighbors in this picture, not competitors: they gate on rules and metrics, this gates on agreements written in plain language, by meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 1: inside the AI agent, before the commit
&lt;/h2&gt;

&lt;p&gt;Code isn't only typed by hand in an editor anymore - more and more of it is written by an AI agent (Claude Code, Cursor, Codex and their relatives). By the time a pull request is open it is too late: you are tired and you want to merge. So the first step of the ladder sits &lt;strong&gt;inside the agent's loop&lt;/strong&gt;: the same rules before it starts writing, and the same review before it says "done".&lt;/p&gt;

&lt;p&gt;It is set up with one line you paste &lt;strong&gt;into the agent's chat&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run reviewgate help agent-setup and do what it says
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instructions are written for the agent, not for a human, and that is deliberate: there are many clients out there, and the agent knows the shape of its own environment better than I can guess. It will check whether things are already configured (otherwise it writes a second configuration on top of a working one, and the final check passes against the old one), and it will put the MCP server where its client expects it - for Claude Code, &lt;code&gt;.mcp.json&lt;/code&gt; in the repository root, &lt;strong&gt;at project level&lt;/strong&gt;, so it travels with the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reviewgate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reviewgate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and it will add two lines to the project instruction file (&lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;.cursorrules&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project standards come from mcp__reviewgate__get_team_rules — call it BEFORE writing code.
Before finishing a task and before git push, check the changes with the mcp__reviewgate__review_changes tool.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without those lines many clients load tool descriptions lazily, and the agent never finds out the tools exist. There are two of them: &lt;code&gt;get_team_rules&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; the code, &lt;code&gt;review_changes&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; "done", with the arguments &lt;code&gt;scope: uncommitted | staged | {base, head}&lt;/code&gt; and &lt;code&gt;mode: full | fast&lt;/code&gt;. Any other value is a tool error, not a silent default.&lt;/p&gt;

&lt;p&gt;Two things I want to be straight about.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A human grants the permission to call tools&lt;/strong&gt;, through their client's dialog. The agent does not grant it to itself, even knowing the file format. That is a barrier, not an inconvenience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No tool call, no review.&lt;/strong&gt; An agent without access to the tool will not say "I can't" - it will simply read the code itself and write its own remarks. In form that is a review; in substance it isn't: no team rules, no judge, no exit code behind that text. The only sign that it worked is that the call happened and the rules came back. Checking takes one phrase, same as setting it up:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  call get_team_rules and show me what came back
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Besides MCP tools there is one more place to put a gate inside an agent: &lt;strong&gt;the client's hooks&lt;/strong&gt; - commands the client runs itself on certain events. ReviewGate can be such a command (&lt;code&gt;reviewgate review --hook-stdin&lt;/code&gt;), and two events are useful here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;before &lt;code&gt;git push&lt;/code&gt;&lt;/strong&gt; - the agent is about to send code out, the hook runs a review and refuses the push on blocking findings (in Claude Code this is the &lt;code&gt;PreToolUse&lt;/code&gt; event for &lt;code&gt;Bash&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;before "done"&lt;/strong&gt; - the agent considers the task finished, and the hook runs a review before it gets to announce that (the &lt;code&gt;Stop&lt;/code&gt; event).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Claude Code that is one entry in the hook settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reviewgate review --hook-stdin"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failure of the review itself (no network, a revoked key) is not turned into a block by the hook - it warns and lets through, like every other gate here.&lt;/p&gt;

&lt;p&gt;Here is a real session, from the sandbox of this article:&lt;/p&gt;

&lt;p&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%2F8rj19lkpkmdftlhpnf64.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%2F8rj19lkpkmdftlhpnf64.png" alt="An agent session transcript: rules before the code, review before " width="800" height="1179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things in that transcript are worth pausing on.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The team rule &lt;code&gt;authorization-is-server-side&lt;/code&gt; made the agent &lt;strong&gt;write a server-side check&lt;/strong&gt; (a 403 for non-admins in the dev stub) instead of settling for a hidden button. The rule did that, not the model's good taste - and the rule is four lines of YAML in the repository.&lt;/li&gt;
&lt;li&gt;The judge &lt;strong&gt;downgraded&lt;/strong&gt; the generator's own finding from major to minor, with its reason attached: &lt;em&gt;"Missing in-flight guard on data-changing delete allows double-submit; real UX concern but severity major is too high for a non-bug guard gap."&lt;/em&gt; Then the agent fixed it and ran the review again - and on the second run the judge dropped another candidate as "no actual defect". The ladder works in both directions: it finds, and it takes back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a live epilogue to this step, which happened while I was preparing the sandbox for this article. The "Call the customer" button in it was written by an agent: by the rules, through &lt;code&gt;get_team_rules&lt;/code&gt;, with &lt;code&gt;review_changes&lt;/code&gt; before "done". I committed its work as it was - 18 lines in three files. Pre-commit (fast mode, threshold &lt;code&gt;blocker&lt;/code&gt;) left one minor: &lt;code&gt;telHref()&lt;/code&gt; re-implements phone normalization outside the existing &lt;code&gt;sdPhone&lt;/code&gt; pipe. The commit went through. Then pre-push (full run, threshold &lt;code&gt;major&lt;/code&gt;) put the same finding in front of a judge, and the judge &lt;strong&gt;took it away&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;❌ &lt;code&gt;format-through-pipes&lt;/code&gt; — "telHref builds a tel: URI (digits only), not display formatting; sdPhone's human format wouldn't fit, so false binding to format-through-pipes."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Note what happened here: the fast mode saw the same thing the full run saw, and the full run was not just stricter, it was more &lt;em&gt;accurate&lt;/em&gt;. The push went through with a clean gate. Both the finding and the verdict are in the &lt;a href="https://github.com/ReviewGate/service-desk/tree/feat/call-button" rel="noopener noreferrer"&gt;&lt;code&gt;feat/call-button&lt;/code&gt;&lt;/a&gt; branch of the sandbox, as they happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gates 2 and 3: pre-commit and pre-push
&lt;/h2&gt;

&lt;p&gt;Here are the recipes you can copy. Strictness grows by steps - the further from the keyboard, the stricter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On commit, fast mode with the threshold &lt;code&gt;blocker&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;--fast&lt;/code&gt; is one model, no judge: twice as fast and three times cheaper. A commit is my draft: I want to see the findings immediately, but I don't block my own draft.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On push, a full run with a judge and the threshold &lt;code&gt;major&lt;/code&gt;.&lt;/strong&gt; This is the boundary where code leaves for the remote repository, and here I am willing to wait for a verdict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hooks go through husky - &lt;a href="https://github.com/ReviewGate/service-desk/blob/main/.husky/pre-commit" rel="noopener noreferrer"&gt;&lt;code&gt;.husky/pre-commit&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run lint &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="c"&gt;# The review is controlled without editing the hook — local .git/config, never committed:&lt;/span&gt;
&lt;span class="c"&gt;#   turn it off for yourself: git config reviewgate.hook false · turn it back on: git config --unset reviewgate.hook&lt;/span&gt;
&lt;span class="c"&gt;#   skip the whole hook once: --no-verify&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git config &lt;span class="nt"&gt;--get&lt;/span&gt; reviewgate.hook&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; reviewgate &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"reviewgate: not installed — review skipped"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# A quick review of what I'm committing (--staged): I see the findings right at commit time.&lt;/span&gt;
&lt;span class="c"&gt;# Blocks on blocker only; the full judge pass happens on push (.husky/pre-push).&lt;/span&gt;
&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
reviewgate review &lt;span class="nt"&gt;--staged&lt;/span&gt; &lt;span class="nt"&gt;--fast&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; blocker &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;$code&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;0&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  2&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"reviewgate: blocker findings — commit stopped"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"reviewgate: the review did not run (failure) — commit allowed unchecked"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2 &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and &lt;a href="https://github.com/ReviewGate/service-desk/blob/main/.husky/pre-push" rel="noopener noreferrer"&gt;&lt;code&gt;.husky/pre-push&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .husky/pre-push — a full review of what's leaving: from origin/main to the tip.&lt;/span&gt;
&lt;span class="c"&gt;# Blocks ONLY on code 2 (threshold not met). Code 1 means the review itself failed&lt;/span&gt;
&lt;span class="c"&gt;# (network, key): warn and let it through — a broken reviewer must not lock up the work.&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git config &lt;span class="nt"&gt;--get&lt;/span&gt; reviewgate.hook&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; reviewgate &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"reviewgate: not installed — review skipped"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
reviewgate review &lt;span class="nt"&gt;--refs&lt;/span&gt; origin/main &lt;span class="nt"&gt;--fail-on&lt;/span&gt; major &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;$code&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;0&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  2&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"reviewgate: blocking findings — push stopped"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"reviewgate: the review did not run (failure) — push allowed unchecked"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2 &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order inside the hook is the principle from the first part: &lt;strong&gt;lint first, paid review after&lt;/strong&gt;. If lint fails, the model is never called. And the &lt;code&gt;|| exit 1&lt;/code&gt; after lint is not redundant: under husky the hook already runs with &lt;code&gt;-e&lt;/code&gt;, but in a plain git hook without it a failing lint blocks nothing - the review runs anyway and the commit goes through.&lt;/p&gt;

&lt;p&gt;Three sentences without which this recipe is dangerous. Each of them I learned on myself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The gate defaults to &lt;code&gt;off&lt;/code&gt;.&lt;/strong&gt; With no explicit &lt;code&gt;--fail-on&lt;/code&gt;, the exit code is decided by the team gate from the config, and that one is off by default: &lt;code&gt;review&lt;/code&gt; returns &lt;code&gt;0&lt;/code&gt; whatever it finds, and such a hook &lt;strong&gt;never blocks&lt;/strong&gt;. If I didn't say this, you would set up a dead hook and think you were protected. That's why both hooks pass the threshold explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit code &lt;code&gt;1&lt;/code&gt; lets you through, and that is not an embarrassment.&lt;/strong&gt; The provider drops the stream, the key gets revoked, the network blinks - "push allowed unchecked" goes to stderr and the push happens. The fail-open is deliberate; the silence is not part of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A home model scheme replaces the team one entirely.&lt;/strong&gt; The role scheme arrives from the team config, but you can override it in your personal &lt;code&gt;~/.config/reviewgate/config.yml&lt;/code&gt; - and a declared home scheme &lt;strong&gt;replaces&lt;/strong&gt; the team one rather than blending into it. Declare only a provider and a key at home and the roles come from the team, and &lt;code&gt;doctor&lt;/code&gt; will say so: "from the team policy". To run exactly what the bot will run, use &lt;code&gt;reviewgate review --team-llm&lt;/code&gt;. Declaring judges without generators at home refuses to start rather than quietly filling in the gap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The switch at the top of both hooks is how you control the review &lt;strong&gt;without editing code&lt;/strong&gt;. The &lt;code&gt;reviewgate.hook&lt;/code&gt; key lives in the local &lt;code&gt;.git/config&lt;/code&gt; of one clone: it is not versioned and never reaches the repository, so every developer decides for themselves while the hook in the repo stays the same for everyone. Review is on by default; &lt;code&gt;git config reviewgate.hook false&lt;/code&gt; turns it off for you, &lt;code&gt;git config --unset reviewgate.hook&lt;/code&gt; brings it back, and &lt;code&gt;--no-verify&lt;/code&gt; skips the hook once. If your team wants the hook to be an &lt;strong&gt;opt-in&lt;/strong&gt; instead, flip the comparison and it stays quiet for everyone who hasn't switched it on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git config &lt;span class="nt"&gt;--get&lt;/span&gt; reviewgate.hook&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0   &lt;span class="c"&gt;# turn it on for yourself: git config reviewgate.hook true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now about money, because you are going to ask: why pay for a review twice, locally and then again on the pull request? Because these are two different steps with two different owners.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before the commit, the code is the developer's own business.&lt;/strong&gt; If it matters to them to show good code &lt;em&gt;before&lt;/em&gt; it lands in git history, they have a way to do that. If it doesn't, they are free not to use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On the pull request, the team pays.&lt;/strong&gt; That is a normal appetite for product quality, not duplication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Your own key, the company's key or a local model - on the developer's step, the one who chooses the step is the one who pays.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is gate 2 on the task of the week - a commit through pre-commit, a live run, output shortened with ellipses, the wording of the findings untouched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"search across requests: search box and status filter"&lt;/span&gt;
&lt;span class="go"&gt;… (npm run lint — green) …
The MR adds a search/status toolbar to the request list and mostly follows modern Angular conventions
(signals, OnPush, control flow). The main risks are in the search pipeline: HTTP calls are placed outside
core/api.service.ts, every keystroke fires an unmanaged subscription without debounce/cancellation,
outdated responses can overwrite newer ones, and search errors are silently shown as an empty list.

🔴 `request-search.service.ts:25` — Network access is outside the single allowed place. This service injects
   HttpClient and hardcodes `/api/requests/search`, while the team rule says all HTTP calls live in
   core/api.service.ts … _(team:http-only-in-api-service)_
🔴 `request-list.component.ts:78` — runSearch() creates a new bare subscription on every keystroke without
   takeUntilDestroyed or any lifecycle management. The previous HTTP request is not cancelled, so an
   outdated response can overwrite a newer one … _(team:no-leaking-subscriptions)_
🟠 … four more major: debounce, three states on load, a dead BehaviorSubject in the new service,
   an unencoded query in the URL …
🔵 `request-list.component.ts:83` — foundAt is formatted by concatenating raw getHours()/getMinutes(),
   so times like 09:05 render as '9:5' … _(team:format-through-pipes)_

✅ Gate (blocker) passed. Findings: 7 (⛔ 0 · 🔴 2 · 🟠 4 · 🔵 1 · ⚪ 0).
&lt;/span&gt;&lt;span class="gp"&gt;⚙️ Run cost: ≈ 0.03 $&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
**🔬 Run diagnostics**
**Model context**: diff: 4 files · full files: 10 (12K chars) · environment: ✓ · from team standards (8 rules): 6 of 7 findings
**Calls**:
- generator `deepseek-v4-flash` — 10.8K→19.6K tokens · 173 s · findings: 7
&lt;/span&gt;&lt;span class="gp"&gt;_No judging took place: fast mode — a single generator, no judge. Findings are published as they are;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;a full run (without `--fast`) confirms them with the second generate→verify pass._

🏷️ License: Community · ❤️ Support the project: https://reviewgate.dev/donate
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the gate line carefully - everyone trips on it, myself included: there are two red findings on the screen and the commit &lt;strong&gt;went through&lt;/strong&gt;. The parenthesis holds the threshold of &lt;em&gt;this&lt;/em&gt; run: &lt;code&gt;blocker&lt;/code&gt;. Critical is serious, but it is a question for the push, not for a draft; the push is where it gets stopped. And the second thing: fast mode brought both criticals and six of seven findings from team rules - but &lt;strong&gt;with no judge&lt;/strong&gt;. Nobody checked them, and the 🔬 block says so in plain words. What you see at commit time is a draft of a review; the verdict comes at push.&lt;/p&gt;

&lt;p&gt;Now the push of that same commit. Same diff, but a full run with a judge and the threshold &lt;code&gt;major&lt;/code&gt;; live, shortened with ellipses, the remote address removed. The line numbers in findings come from the model and can drift by a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin feat/search
&lt;span class="go"&gt;The MR adds client-side search/filter UI backed by a new /api/requests/search call. Main problems:
the network call violates http-only-in-api-service, the component's search subscription is not
lifecycle-managed, search requests are neither debounced nor cancelled, errors are silently shown
as an empty list, and the URL is built without encoding.

🔴 `request-search.service.ts:15` — RequestSearchService injects HttpClient and calls
   '/api/requests/search' directly. Team rule http-only-in-api-service requires all network calls to
   live only in core/api.service.ts … _(team:http-only-in-api-service)_
🔴 `request-list.component.ts:78` — subscribe() in runSearch() is not tied to the component lifecycle:
   if the manager leaves the screen before the response arrives, the callback still runs and writes into
   signals of a destroyed component … _(team:no-leaking-subscriptions)_
🟠 … five more major: no debounce, no cancellation of stale requests, error rendered as an empty list,
   a new BehaviorSubject instead of signals, an unencoded query parameter …

❌ Gate (major) FAILED. Findings: 8 (⛔ 0 · 🔴 2 · 🟠 5 · 🔵 1 · ⚪ 0).
&lt;/span&gt;&lt;span class="gp"&gt;⚙️ Run cost: ≈ 0.06 $&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
**🔬 Run diagnostics**
**Calls**:
- generator `deepseek-v4-flash` — 10.8K→12.6K tokens (cache: 10.8K) · 105 s · findings: 8
- validator `deepseek-v4-pro` — 10.7K→5.6K tokens · 114 s · dropped: 0 · downgraded: 0
**The judge** confirmed every finding (dropped 0, downgraded 0).

reviewgate: blocking findings — push stopped
error: failed to push some refs to '…'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare it with the commit: there, seven findings with no judge and a green &lt;code&gt;blocker&lt;/code&gt; gate; here, the same diff went through a judge that confirmed all eight, two of them critical - and the &lt;code&gt;major&lt;/code&gt; gate is red. The push is stopped; next come the fixes and another push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gate 4: the pull request, a gate on merge rather than a chat
&lt;/h2&gt;

&lt;p&gt;Now there are several of us, the repository is on GitHub, and the review has to stand on the road to the merge rather than in my terminal. Same engine, same policy from the same &lt;code&gt;.reviewgate/config.yml&lt;/code&gt; - but a bot, and here the gate is not an exit code any more. It is a &lt;strong&gt;check run&lt;/strong&gt; on the commit (or a plain commit status, if the bot runs on a token rather than as a GitHub App).&lt;/p&gt;

&lt;p&gt;It goes up in an evening, without reading the docs. Two ways, pick one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Way 1: hand it to your agent.&lt;/strong&gt; The &lt;code&gt;ai-setup.md&lt;/code&gt; playbook is written for an agent rather than for a human. Its step-by-step part is written around GitLab, with a GitHub section that sends the agent to &lt;a href="https://reviewgate.dev/docs/github" rel="noopener noreferrer"&gt;/docs/github&lt;/a&gt; for the credentials and the webhook. The agent will ask you for the App credentials (or a token) and the model key, show you what it is about to write into &lt;code&gt;.env&lt;/code&gt; and which webhook it will create, and verify every step. One phrase into the chat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set up ReviewGate for our GitHub org following https://reviewgate.dev/ai-setup.md - ask me for the secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Way 2: by hand.&lt;/strong&gt; The terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;reviewgate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;reviewgate
curl &lt;span class="nt"&gt;-O&lt;/span&gt; https://reviewgate.dev/docker-compose.yml
&lt;span class="c"&gt;# put an .env next to it (below), then:&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
curl http://localhost:3000/api/health        &lt;span class="c"&gt;# {"status":"ok", …}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; holds the GitHub block and the model block (keep one of the variants). The full variable reference is at &lt;a href="https://reviewgate.dev/docs/install" rel="noopener noreferrer"&gt;/docs/install&lt;/a&gt;, the model providers with all their keys at &lt;a href="https://reviewgate.dev/docs/llm" rel="noopener noreferrer"&gt;/docs/llm&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# --- GitHub, App mode (recommended) ---&lt;/span&gt;
&lt;span class="nv"&gt;GITHUB_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;a-long-random-string
&lt;span class="nv"&gt;GITHUB_APP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;…
&lt;span class="nv"&gt;GITHUB_APP_PRIVATE_KEY_B64&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;…        &lt;span class="c"&gt;# base64 -w0 key.pem  (macOS: base64 -i key.pem | tr -d '\n')&lt;/span&gt;

&lt;span class="c"&gt;# --- or PAT mode (a quick trial) ---&lt;/span&gt;
&lt;span class="c"&gt;# GITHUB_TOKEN=github_pat_…&lt;/span&gt;

&lt;span class="c"&gt;# --- The model, variant A: Anthropic ---&lt;/span&gt;
&lt;span class="nv"&gt;LLM_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;anthropic
&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;…
&lt;span class="c"&gt;# ANTHROPIC_MODEL=claude-sonnet-5               # claude-opus-4-8 by default&lt;/span&gt;

&lt;span class="c"&gt;# --- variant B: DeepSeek or any other OpenAI-compatible endpoint ---&lt;/span&gt;
&lt;span class="c"&gt;# LLM_PROVIDER=openai&lt;/span&gt;
&lt;span class="c"&gt;# LLM_BASE_URL=https://api.deepseek.com/v1&lt;/span&gt;
&lt;span class="c"&gt;# LLM_MODEL=deepseek-v4-flash&lt;/span&gt;
&lt;span class="c"&gt;# LLM_API_KEY=…&lt;/span&gt;
&lt;span class="c"&gt;# LLM_JSON_MODE=none&lt;/span&gt;
&lt;span class="c"&gt;# LLM_MAX_TOKENS=64000&lt;/span&gt;

&lt;span class="c"&gt;# --- variant C: a local model, the code never leaves your network ---&lt;/span&gt;
&lt;span class="c"&gt;# LLM_PROVIDER=ollama&lt;/span&gt;
&lt;span class="c"&gt;# LLM_BASE_URL=http://your-ollama-host:11434/v1&lt;/span&gt;
&lt;span class="c"&gt;# LLM_MODEL=qwen3-coder:30b-a3b-q8_0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the GitHub side, a &lt;strong&gt;GitHub App&lt;/strong&gt; is the proper way in: the bot gets an identity of its own, short-lived per-installation tokens and a gate through the Checks API. The permissions it needs are exactly five, and I measured them by running into each missing one (details at &lt;a href="https://reviewgate.dev/docs/github" rel="noopener noreferrer"&gt;/docs/github&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;permission&lt;/th&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;what for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pull requests&lt;/td&gt;
&lt;td&gt;read and write&lt;/td&gt;
&lt;td&gt;the diff, the summary, inline comments, replies in threads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contents&lt;/td&gt;
&lt;td&gt;read-only&lt;/td&gt;
&lt;td&gt;repository files: the team config and neighboring files for the judge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checks&lt;/td&gt;
&lt;td&gt;read and write&lt;/td&gt;
&lt;td&gt;the severity gate as a check run, if you turned it on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Issues&lt;/td&gt;
&lt;td&gt;read-only&lt;/td&gt;
&lt;td&gt;only so that the Issue comment event exists - without it the bot never sees a question under the summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metadata&lt;/td&gt;
&lt;td&gt;read-only&lt;/td&gt;
&lt;td&gt;GitHub always requires it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Subscribe to four events: &lt;strong&gt;Pull request&lt;/strong&gt;, &lt;strong&gt;Pull request review comment&lt;/strong&gt;, &lt;strong&gt;Pull request review&lt;/strong&gt;, &lt;strong&gt;Issue comment&lt;/strong&gt;. Then the webhook: URL &lt;code&gt;https://your-bot-host/api/webhooks/github&lt;/code&gt;, content type &lt;code&gt;application/json&lt;/code&gt;, secret equal to &lt;code&gt;GITHUB_WEBHOOK_SECRET&lt;/code&gt;. The bot verifies &lt;code&gt;X-Hub-Signature-256&lt;/code&gt; over the raw body in constant time; an unsigned request gets a &lt;code&gt;401&lt;/code&gt;, a valid one gets &lt;code&gt;202&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One trap worth knowing before it costs you an evening: &lt;strong&gt;changing an App's permissions takes two steps&lt;/strong&gt;. Ticking a box changes the App; every installation then has to accept the new set (organization settings → GitHub Apps → Configure → "Accept new permissions"). Until that happens GitHub keeps sending the old set of events, and from the outside it looks exactly like "the bot is silent" - with not one error in the logs.&lt;/p&gt;

&lt;p&gt;A personal access token works too (Pull requests write, Contents read, Commit statuses write), with three differences: comments come from your own account, the gate degrades from a check run to a commit status, and &lt;strong&gt;the bot never answers comments from the account whose token it runs on&lt;/strong&gt; - that is the anti-loop guard. In PAT mode the conversation is open to everyone except you.&lt;/p&gt;

&lt;p&gt;In the team config, the same file from the rules chapter changes in exactly these places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# …&lt;/span&gt;
&lt;span class="na"&gt;severity_gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;major&lt;/span&gt;         &lt;span class="c1"&gt;# was off: blocking findings → a red commit status&lt;/span&gt;

&lt;span class="na"&gt;llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;generators&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt; &lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;claude-sonnet-5&lt;/span&gt; &lt;span class="pi"&gt;}]&lt;/span&gt;               &lt;span class="c1"&gt;# was deepseek-v4-flash&lt;/span&gt;
  &lt;span class="na"&gt;judges&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="pi"&gt;[{&lt;/span&gt; &lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;effort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;high&lt;/span&gt; &lt;span class="pi"&gt;}]&lt;/span&gt; &lt;span class="c1"&gt;# was deepseek-v4-pro&lt;/span&gt;
  &lt;span class="c1"&gt;# …&lt;/span&gt;

&lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;              &lt;span class="c1"&gt;# ❓ questions to the author, built from the judge's rejects&lt;/span&gt;
&lt;span class="na"&gt;reply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;              &lt;span class="c1"&gt;# replies in threads (up to 3 bot replies per thread by default)&lt;/span&gt;
&lt;span class="c1"&gt;# …&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With branch protection requiring checks to pass, a red check run &lt;strong&gt;locks the Merge button&lt;/strong&gt; - and that is the gate. Here is what the team sees on the pull request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one summary comment that &lt;strong&gt;lives and gets rewritten in place&lt;/strong&gt; on new commits (not a second one, not a third - the same one), with a digest of the findings and a cost line;&lt;/li&gt;
&lt;li&gt;inline threads anchored to diff positions, and on mechanical fixes a native suggestion block with a "Commit suggestion" button;&lt;/li&gt;
&lt;li&gt;the line &lt;code&gt;❌ Severity gate (major) failed: N blocking findings&lt;/code&gt; → the check run turns red; after a fix comes an incremental run, previously open findings get closed, and the gate remembers them and only lets the pull request go when all of them are gone;&lt;/li&gt;
&lt;li&gt;a conversation in the thread: the bot answers an objection under its own finding, and answers a mention of its account under the summary;&lt;/li&gt;
&lt;li&gt;findings that come from your team's rules carry the 📐 badge next to the id - the one promised in the first chapter:
&lt;code&gt;🔴 http-only-in-api-service 📐 — ExportCsvService injects HttpClient and calls '/api/requests' directly …&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is that gate on a real pull request: &lt;a href="https://github.com/ReviewGate/service-desk/pull/2" rel="noopener noreferrer"&gt;#2 in the sandbox&lt;/a&gt;, a CSV export the managers asked for. Five findings, three of them blocking, &lt;code&gt;Severity gate (major) failed&lt;/code&gt;, check run red, ≈0.25 $ for the run.&lt;/p&gt;

&lt;h3&gt;
  
  
  The conversation, where a text generator and a reviewer part ways
&lt;/h3&gt;

&lt;p&gt;Agreement is the most probable next token. A reviewer weighs the argument instead: sometimes the argument wins, sometimes it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thread one, escaping the CSV.&lt;/strong&gt; The bot's finding was the classic one: fields joined with a bare comma, no quoting, and a value starting with &lt;code&gt;=&lt;/code&gt; becomes a formula when the file opens in Excel. My objection as the author:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Request subjects are typed by our own managers, there is no external data on this panel - isn't escaping overkill for an internal tool?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The bot did not fold. It split the answer in two: column corruption is a correctness bug that stands on its own even if every field were trusted, and &lt;code&gt;clientName&lt;/code&gt;/&lt;code&gt;clientPhone&lt;/code&gt; come from the customer, not from the manager, so untrusted input does reach an Excel file. It kept the finding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thread two, the date format.&lt;/strong&gt; The finding was minor: the export glues the date together by hand instead of going through the app's pipes, so the file and the screen disagree. My objection:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The CSV is opened in Excel, which re-formats dates on its own anyway, and the file is a machine artefact rather than the screen. Does the export really have to match the table format?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This time the bot &lt;strong&gt;gave up half of its own finding&lt;/strong&gt; and kept the half it could prove:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fair point on dates — Excel does interpret and re-render &lt;code&gt;M/D/YYYY&lt;/code&gt; on its own, so matching the on-screen format there is not important; that half of the comment can be dropped. Phone numbers are a different story though: Excel treats unquoted digit strings as numbers, so a phone like &lt;code&gt;0501234567&lt;/code&gt; can lose the leading zero or get turned into scientific notation once opened.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&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%2F1v1s0ffbaou0velgrg54.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%2F1v1s0ffbaou0velgrg54.png" alt="The dialogue in the pull request threads" width="800" height="803"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There was a third exchange, later, and it is my favorite line the bot has produced. After the fixes I objected that the error handling now lives in the component, not in the service. The bot agreed - and drew the boundary of its own knowledge without being asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;… no additional &lt;code&gt;catchError&lt;/code&gt; is required in the service itself … &lt;strong&gt;I can't see &lt;code&gt;request-list.component.ts&lt;/code&gt; in this diff, so I'm taking your description at face value&lt;/strong&gt;; if that's accurate, this thread can be closed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The most useful finding of the whole pull request was in the fix
&lt;/h3&gt;

&lt;p&gt;I pushed the fixes - the request through &lt;code&gt;ApiService&lt;/code&gt;, escaping, &lt;code&gt;formatDate&lt;/code&gt;, the phone through the pipe, a busy state on the button. The summary was rewritten in place, &lt;code&gt;Incremental review: 3 of 4 changed files checked&lt;/code&gt;. And the incremental run found this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🟠 &lt;strong&gt;export-triggers-shared-cache-refetch&lt;/strong&gt; — buildAndDownload() calls this.api.loadRequests(), which internally does tap((items) =&amp;gt; this.requestsSubject.next(items)). That subject backs requests$, the same stream the request table's async pipe renders … So clicking 'Export CSV' silently triggers an extra network round trip and overwrites the visible list's data behind the manager's back, potentially reshuffling rows they're currently reading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had introduced that while fixing something else, which is where this class of tool earns its keep: not on the sins you plant on purpose, but on the ones you add while removing the previous ones. The judge, in the same run, dropped a speculative null-safety candidate: &lt;em&gt;"ServiceRequest fields are typed string; null-safety concern is speculation not backed by code."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One more fix (a read-only &lt;code&gt;fetchAllRequests()&lt;/code&gt; that doesn't touch the cache), all threads resolved, and the next run closed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Severity gate (major) passed.
✅ No findings.
Open findings on this PR: 0.
🧾 Total for this PR (4 runs + 3 replies): ≥ 167.1K tokens in · 39.7K out · ≥ 1.01 $
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check run went green and Merge unlocked. One honest detail from that last stretch: resolving the threads does not recompute the gate by itself - the recount happens on the next run. So the green status arrived with the next commit, not the moment I clicked "Resolve conversation".&lt;/p&gt;

&lt;p&gt;And the numbers, from the footer of that same summary (the cost line is switched on with &lt;code&gt;cost.show&lt;/code&gt;, so the whole team sees it):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;one review&lt;/strong&gt; on the "sonnet finds → opus judges" scheme: generator 109 s, judge 23 s, about two minutes and ≈0.25 $. Note the judge: 23 seconds. An expensive model is not a slow one;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;a reply in a thread&lt;/strong&gt;: 15-23 s and 0.02-0.06 $;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the whole pull request&lt;/strong&gt;: 4 runs and 3 replies, ≥1.01 $ by the counter in the summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a small feature in four files reviewed by an expensive pair of models. How the numbers change from step to step is in the cost table below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closed network: when cloud LLMs are off limits
&lt;/h2&gt;

&lt;p&gt;The last check of the frame: does any of this survive in a place where source code must not leave the network? The question is really just "where does the model run". I switch the provider in my personal config to a local one (Ollama, an OpenAI-compatible endpoint, no keys involved) and declare both roles on the local model. &lt;strong&gt;Rules, thresholds and hooks are untouched:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;llm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama&lt;/span&gt;
  &lt;span class="na"&gt;base_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:11434/v1&lt;/span&gt;
  &lt;span class="na"&gt;generators&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt; &lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;qwen3-coder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;30b-a3b-q8_0&lt;/span&gt; &lt;span class="pi"&gt;}]&lt;/span&gt;
  &lt;span class="na"&gt;judges&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;     &lt;span class="pi"&gt;[{&lt;/span&gt; &lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;qwen3-coder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;30b-a3b-q8_0&lt;/span&gt; &lt;span class="pi"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That part works exactly as advertised: the same policy, the same gate, the same exit codes, and after four review runs &lt;code&gt;nettop&lt;/code&gt; on the &lt;code&gt;ollama serve&lt;/code&gt; process still reports zero bytes in and zero bytes out. And then the honest part, which I am not going to dress up.&lt;/p&gt;

&lt;p&gt;I ran both diffs from this article through that setup on an Apple Silicon laptop. Here is what came back:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;diff&lt;/th&gt;
&lt;th&gt;mode&lt;/th&gt;
&lt;th&gt;time&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSV export&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;259 s&lt;/td&gt;
&lt;td&gt;3 findings, all &lt;code&gt;info&lt;/code&gt;, all noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSV export&lt;/td&gt;
&lt;td&gt;full&lt;/td&gt;
&lt;td&gt;374 s&lt;/td&gt;
&lt;td&gt;1 &lt;code&gt;info&lt;/code&gt; finding; the judge stripped its fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task of the week&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;td&gt;374 s&lt;/td&gt;
&lt;td&gt;3 findings, all &lt;code&gt;info&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;task of the week&lt;/td&gt;
&lt;td&gt;full&lt;/td&gt;
&lt;td&gt;590 s&lt;/td&gt;
&lt;td&gt;the same 3, and the local judge confirmed all of them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not one of the real defects. On the search diff the model reported three times that &lt;code&gt;HttpClient&lt;/code&gt; "is unused" in a service that uses it two lines below. The cloud pair found the leaking subscription, the missing debounce, the swallowed error and the CSV injection on those same diffs.&lt;/p&gt;

&lt;p&gt;Three conclusions, in order of usefulness.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The machinery is not what degrades - the model is.&lt;/strong&gt; Rules, judging, gates, diagnostics, exit codes: all identical down to the line. What collapsed was the quality of the findings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A weak judge is worse than no judge.&lt;/strong&gt; In the last run the local judge confirmed three worthless findings. A judge is only a filter while it is stronger than the generator; make both of them the same weak model and you get a rubber stamp. If you have one strong model and one weak one, the strong one belongs in the judge's seat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A laptop is not what a closed network runs on.&lt;/strong&gt; Mine is a 30-billion-parameter model in an 8-bit quant on a personal machine - a personal experiment on personal hardware. An organization that cannot use the cloud puts a GPU server under the model and runs something bigger; the gap narrows and "zero bytes out" stays exactly the same. How much of the gap your hardware closes is for you to measure - I don't have those numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If that is your world, the working recipe is: keep the fast gate close to the keyboard, put the full run on push and on the pull request, and give the judge the strongest model you are allowed to run. vLLM, your own gateway, anything OpenAI-compatible: it needs an address and a model name.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, step by step
&lt;/h2&gt;

&lt;p&gt;All the numbers are live runs from this article: two small diffs (the task of the week - 4 files, ~140 lines; and the CSV export), an 18-line agent diff and a one-paragraph documentation change, one to four runs each, measured on 24 August 2026. This is not a benchmark, it is an order of magnitude. The formula to remember is "seconds to minutes, cents to a dollar".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;step / mode&lt;/th&gt;
&lt;th&gt;models&lt;/th&gt;
&lt;th&gt;diff&lt;/th&gt;
&lt;th&gt;time&lt;/th&gt;
&lt;th&gt;cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gate 1, &lt;code&gt;review_changes&lt;/code&gt; in the agent&lt;/td&gt;
&lt;td&gt;sonnet-5 → judge opus-4-8&lt;/td&gt;
&lt;td&gt;"Delete request" button, 5 files&lt;/td&gt;
&lt;td&gt;165 s; the re-check after the fix 272 s&lt;/td&gt;
&lt;td&gt;0.30 $; 0.52 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate 2, pre-commit, fast&lt;/td&gt;
&lt;td&gt;DeepSeek flash&lt;/td&gt;
&lt;td&gt;task of the week, 4 files&lt;/td&gt;
&lt;td&gt;173 s&lt;/td&gt;
&lt;td&gt;≈0.03 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate 2 → 3 on the agent's diff (18 lines)&lt;/td&gt;
&lt;td&gt;sonnet-5 → judge opus-4-8&lt;/td&gt;
&lt;td&gt;the call button&lt;/td&gt;
&lt;td&gt;35 s / 70 s&lt;/td&gt;
&lt;td&gt;0.06 $ / 0.17 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate 3, pre-push, full&lt;/td&gt;
&lt;td&gt;flash → judge pro&lt;/td&gt;
&lt;td&gt;task of the week, 4 files&lt;/td&gt;
&lt;td&gt;222 s&lt;/td&gt;
&lt;td&gt;0.06 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate 3, pre-push on a trivial change&lt;/td&gt;
&lt;td&gt;sonnet-5&lt;/td&gt;
&lt;td&gt;1 file, a paragraph of docs&lt;/td&gt;
&lt;td&gt;12 s&lt;/td&gt;
&lt;td&gt;≈0.01 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate 4, pull request review&lt;/td&gt;
&lt;td&gt;sonnet-5 → judge opus-4-8&lt;/td&gt;
&lt;td&gt;the CSV feature&lt;/td&gt;
&lt;td&gt;~2 min (the judge: 23 s)&lt;/td&gt;
&lt;td&gt;0.25 $; the whole PR (4 runs + 3 replies) ≥1.01 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate 4, a reply in a thread&lt;/td&gt;
&lt;td&gt;generator sonnet-5&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;15-23 s&lt;/td&gt;
&lt;td&gt;0.02-0.06 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;closed network, fast / full&lt;/td&gt;
&lt;td&gt;qwen3-coder:30b-a3b-q8_0, Ollama, laptop&lt;/td&gt;
&lt;td&gt;the CSV feature&lt;/td&gt;
&lt;td&gt;259 s / 374 s&lt;/td&gt;
&lt;td&gt;0 $&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;closed network, fast / full&lt;/td&gt;
&lt;td&gt;the same&lt;/td&gt;
&lt;td&gt;task of the week&lt;/td&gt;
&lt;td&gt;374 s / 590 s&lt;/td&gt;
&lt;td&gt;0 $&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A full run takes minutes, and that is not a bug, it is the ladder: the further from the keyboard, the stricter and the slower. At commit time you wait seconds; at push time you are prepared to wait for a verdict; on the pull request you don't wait at all, it arrives on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is not here
&lt;/h2&gt;

&lt;p&gt;What it doesn't do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;it doesn't find everything&lt;/strong&gt; - you saw the spread;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;it doesn't replace a human reviewer&lt;/strong&gt; - the "human review" step never left the diagram;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;it isn't free in tokens&lt;/strong&gt; - the tool is free, the model is yours and on your bill;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;it isn't open source&lt;/strong&gt; - the binary is closed and lives on donations. I plan to open it later; not today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are no sources to read, so I am not asking you to take my word for it. Here is what you can check yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the review policy is YAML in your repository, and the result is &lt;code&gt;--json&lt;/code&gt; plus exit codes;&lt;/li&gt;
&lt;li&gt;the code and the diff go only to your model with your key. They do not reach me, the author of ReviewGate, and are not stored anywhere;&lt;/li&gt;
&lt;li&gt;on a local model, "zero bytes leaving the machine" is visible in &lt;code&gt;nettop&lt;/code&gt; on the model's process;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SHA256SUMS&lt;/code&gt; sits next to the binaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it yourself
&lt;/h2&gt;

&lt;p&gt;The sandbox with that exact project is &lt;a href="https://github.com/ReviewGate/service-desk" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/ReviewGate/service-desk&lt;/code&gt;&lt;/a&gt;, and its history &lt;em&gt;is&lt;/em&gt; the scenario: the tag &lt;code&gt;before-search&lt;/code&gt; (rules and hooks already in place, no feature yet) → &lt;code&gt;search-unreviewed&lt;/code&gt; (search with its sins: the commit passed pre-commit, the push was stopped by the gate) → &lt;code&gt;main&lt;/code&gt; (fixes from the review, and then the team stage). To repeat gate 3 on that same diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ReviewGate/service-desk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;service-desk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt;   &lt;span class="c"&gt;# the hooks install themselves&lt;/span&gt;
git checkout search-unreviewed
reviewgate review &lt;span class="nt"&gt;--refs&lt;/span&gt; before-search &lt;span class="nt"&gt;--fail-on&lt;/span&gt; major&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# what pre-push does: 2 = push stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model scheme in the repo config is DeepSeek. With a key from another provider, declare your own scheme at home (&lt;code&gt;generators&lt;/code&gt;/&lt;code&gt;judges&lt;/code&gt;) and it replaces the team one entirely - there is an example in the README there. Everything else is at &lt;a href="https://reviewgate.dev/docs" rel="noopener noreferrer"&gt;reviewgate.dev/docs&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  FAQ, before you ask in the comments
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Linux / Windows?&lt;/strong&gt; Six platforms, including musl for Alpine and Windows; the install command for each and the &lt;code&gt;SHA256SUMS&lt;/code&gt; are on &lt;a href="https://reviewgate.dev/docs/agents" rel="noopener noreferrer"&gt;/docs/agents&lt;/a&gt;, and the binaries are mirrored on &lt;a href="https://github.com/ReviewGate/reviewgate/releases" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How is this different from the AI review built into GitHub or GitLab?&lt;/strong&gt; Four checkable things, as of this writing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your team's rules, from your repository;&lt;/li&gt;
&lt;li&gt;your key and your model, down to your own hardware;&lt;/li&gt;
&lt;li&gt;one engine in three places - in the agent, in the hooks, on the pull request;&lt;/li&gt;
&lt;li&gt;a gate you configure, rather than one you accept as given.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I won't take a position on "who is smarter" - that depends on the model, and the model is yours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many false positives?&lt;/strong&gt; I haven't measured it publicly, and the number depends heavily on the provider and the model you pick: DeepSeek and Sonnet/Opus behave differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who uses this?&lt;/strong&gt; There is a pilot in a real team. No name and no numbers - I don't have permission for either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two AI gates, isn't that expensive?&lt;/strong&gt; See the pre-commit and pre-push chapter: before the commit, the one who chooses the step pays (own key, company key, or a local model at 0 $); on the pull request, the team pays, the same way it pays for any other gate.&lt;/p&gt;

</description>
      <category>codereview</category>
      <category>devops</category>
      <category>ai</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
