<?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: Evgenii Menshikov</title>
    <description>The latest articles on DEV Community by Evgenii Menshikov (@artjack).</description>
    <link>https://dev.to/artjack</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%2F4111727%2F3a0c6872-a5f4-4a07-bece-aa607cd6ccad.jpg</url>
      <title>DEV Community: Evgenii Menshikov</title>
      <link>https://dev.to/artjack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/artjack"/>
    <language>en</language>
    <item>
      <title>I built a QA agent that audits itself nightly. It has filed 79 findings against its own releases.</title>
      <dc:creator>Evgenii Menshikov</dc:creator>
      <pubDate>Sat, 05 Sep 2026 23:44:01 +0000</pubDate>
      <link>https://dev.to/artjack/i-built-a-qa-agent-that-audits-itself-nightly-it-has-filed-79-findings-against-its-own-releases-28i2</link>
      <guid>https://dev.to/artjack/i-built-a-qa-agent-that-audits-itself-nightly-it-has-filed-79-findings-against-its-own-releases-28i2</guid>
      <description>&lt;p&gt;Most "AI QA agents" audit your repo from scratch every time and end with LGTM. I wanted the opposite: a tester with memory, a failure taxonomy, a verdict contract, and no ability to touch the code it judges.&lt;/p&gt;

&lt;p&gt;So in &lt;a href="https://github.com/ArtJack/verdict" rel="noopener noreferrer"&gt;Verdict&lt;/a&gt; the model only judges. A stdlib harness measures everything else — timestamps, SHAs, test counts, diff coverage, a signed run history — and a validator refuses any state that claims more than was measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a run looks like
&lt;/h2&gt;

&lt;p&gt;A state file carries the baseline, so every run is a delta:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NEW / STILL_OPEN / RESOLVED / REGRESSED&lt;/code&gt;, regressions ranked first, every finding with a stable ID and an age.&lt;/li&gt;
&lt;li&gt;Every red test is classified: real defect, stale expectation (with a citation), brittle, environment, or flaky — quarantined with an expiry, not deleted.&lt;/li&gt;
&lt;li&gt;Every run ends in one of &lt;code&gt;pass | pass with risks | blocked | fail&lt;/code&gt;, and a pass must name what was &lt;strong&gt;not&lt;/strong&gt; tested.&lt;/li&gt;
&lt;li&gt;It never edits your code: no Edit tool, a hook that confines writes to the QA root, a strict-mode Bash guard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Then I pointed it at itself
&lt;/h2&gt;

&lt;p&gt;Every release is audited by the previous one, nightly. It has filed 79 findings against its own harness so far — including defects in the fixes for earlier findings, three times in a row, until the process itself changed: before closing a finding, name the axis the fix did not vary, and test one point on it.&lt;/p&gt;

&lt;p&gt;The mutation catalogue that scores whether the test suite defends each rule was itself enumerated wrong twice (from the tests, then from the fix list) before it was enumerated from the code — and only then did it find the four gaps no fix-list could reach.&lt;/p&gt;

&lt;p&gt;Everything is published: the eval scores including the misses, the run history, the findings it filed against itself, the accepted risks with their citations.&lt;/p&gt;

&lt;h2&gt;
  
  
  A stranger run: &lt;code&gt;pallets/itsdangerous&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To see what a newcomer hits, I ran the published wheel on a library I had never opened, in a fresh clone, solo mode. Eighteen minutes, verdict &lt;code&gt;pass with risks&lt;/code&gt;, nine findings, all filed as proven, checkout untouched.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sev&lt;/th&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Major&lt;/td&gt;
&lt;td&gt;Signature verification accepts non-canonical base64: four distinct token strings unsign to the same payload, plus a signature with &lt;code&gt;@@@@&lt;/code&gt; or trailing &lt;code&gt;=&lt;/code&gt; appended.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Major&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;max_age&lt;/code&gt; boundary is unpinned — the test steps age 1→11 across &lt;code&gt;max_age=10&lt;/code&gt; and skips the boundary; injecting &lt;code&gt;&amp;gt;=&lt;/code&gt; leaves 297/297 green.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Major&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;hmac.compare_digest&lt;/code&gt; can be replaced with &lt;code&gt;==&lt;/code&gt; and the suite stays green: the library's only timing-attack defence has no guard test.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minor&lt;/td&gt;
&lt;td&gt;Unbounded &lt;code&gt;zlib.decompress&lt;/code&gt; on unverified input via &lt;code&gt;loads_unsafe&lt;/code&gt;: 39 KB → 30 MB. Held to Minor because &lt;code&gt;loads()&lt;/code&gt; verifies before decompressing — measured with a call counter, 0 calls on a bad signature.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minor&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sep in _base64_alphabet&lt;/code&gt; is a substring test, not a set test: &lt;code&gt;Signer(sep=b"az")&lt;/code&gt; is accepted and 12/2000 legitimately signed values fail their own round-trip.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minor&lt;/td&gt;
&lt;td&gt;Three smaller ones: a docstring that promises "never fails" on a call that raises, a coverage config naming a different project, a test parameter that asserts nothing.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The part people actually pay a tester for is the &lt;strong&gt;verified intact&lt;/strong&gt; list: verification precedes decompression; key rotation revokes (mutant → 3 failures); salts separate contexts (mutant → 13 failures); wire-format golden masters pin signatures across versions; suite deterministic across 17 executions.&lt;/p&gt;

&lt;p&gt;And what it asked the maintainers instead of deciding for them: is &lt;code&gt;age == max_age&lt;/code&gt; meant to be accepted? Is token-string canonicality a guarantee, or must callers key off the payload?&lt;/p&gt;

&lt;p&gt;The one thing that broke was Verdict's own runner, not the library: &lt;code&gt;verdict-run .&lt;/code&gt; read &lt;code&gt;.&lt;/code&gt; as a project key, declared a completed run lost, and ran it again. Fixed the same evening. A stranger would have hit that before reading a single finding — which is exactly why the run was done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why won't it fix the bugs?
&lt;/h2&gt;

&lt;p&gt;Independence. A tester that patches what it judges is grading its own homework. It returns an ordered fix list instead, and the next run verifies each fix by re-injecting the defect and watching the suite fail — not by noting the defect's absence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add ArtJack/verdict
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;verdict@verdict
/verdict:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or headless: &lt;code&gt;uvx --from verdict-qa-mcp verdict-run .&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/ArtJack/verdict" rel="noopener noreferrer"&gt;https://github.com/ArtJack/verdict&lt;/a&gt; — MIT, Python 3.9+, stdlib only, no network, no telemetry. If you run it on something and it misses, I want the report: the eval suite publishes its misses, and yours would be the first external one.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>claude</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
