<?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: syntaxixr</title>
    <description>The latest articles on DEV Community by syntaxixr (@syntaxixr).</description>
    <link>https://dev.to/syntaxixr</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%2F4152229%2F8787a0d8-e196-4fde-aaf3-1b90a2eb8ddc.jpg</url>
      <title>DEV Community: syntaxixr</title>
      <link>https://dev.to/syntaxixr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/syntaxixr"/>
    <language>en</language>
    <item>
      <title>On Windows, four processes appending to one file lost 37 of 200 lines</title>
      <dc:creator>syntaxixr</dc:creator>
      <pubDate>Thu, 15 Oct 2026 16:21:01 +0000</pubDate>
      <link>https://dev.to/syntaxixr/on-windows-four-processes-appending-to-one-file-lost-37-of-200-lines-3738</link>
      <guid>https://dev.to/syntaxixr/on-windows-four-processes-appending-to-one-file-lost-37-of-200-lines-3738</guid>
      <description>&lt;p&gt;My CI failed with this, only on the Windows runner, on a commit that changed nothing but a few GIFs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;receipts: Unexpected token '\', "\hostedtoo"... is not valid JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same test had passed on that runner a few hours earlier. Here is what it was, because it is a small bug that is easy to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/syntaxixr/receipts" rel="noopener noreferrer"&gt;Receipts&lt;/a&gt; runs the tests a change added or edited twice: with the change, and with the changed source files put back the way they were. For pytest it loads a tiny plugin that writes one JSON line per test result into a file, and the Node side reads that file afterwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RECEIPTS_PYTEST_OUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One file, opened in append mode, one line per record. Fine, until the project uses pytest-xdist. Then several worker processes run the tests, and at the end every one of them writes a record with the list of modules it imported. Those records are long, full of paths like &lt;code&gt;\hostedtoolcache\windows\Python\...&lt;/code&gt;. That is the fragment in the error: the start of a line that got torn in half.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repro
&lt;/h2&gt;

&lt;p&gt;Four processes, each appending the same long JSON line 50 times to one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\hostedtoolcache\windows\Python\Lib\m%04d.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)]}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;race.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;ps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my Windows machine: &lt;strong&gt;163 lines instead of 200.&lt;/strong&gt; 37 writes simply vanished. With records of different lengths you also get torn lines instead of clean losses, which is what CI hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;On POSIX, &lt;code&gt;O_APPEND&lt;/code&gt; means "move to the end and write" happens as one step for each &lt;code&gt;write()&lt;/code&gt;, so concurrent appenders don't overwrite each other. On Windows, the C runtime emulates append mode: it seeks to the end, then writes. Two processes can both seek to the same end and the second write lands on top of the first. Nothing errors, data is just gone.&lt;/p&gt;

&lt;p&gt;It only showed up now because it needs two xdist workers to finish within the same moment. It had been passing for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Stop sharing the file. The controlling process keeps &lt;code&gt;out.json&lt;/code&gt;, each xdist worker writes &lt;code&gt;out.json.&amp;lt;worker id&amp;gt;&lt;/code&gt;, and the reader picks up all of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workerinput&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%s.%s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workerid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getpid&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While I was there, the reader stopped throwing on a line it can't parse: a run killed for hanging can leave a half-written last line, and the records before it still count.&lt;/p&gt;

&lt;p&gt;The regression test doesn't try to reproduce the race (that would be a flaky test about flakiness). It checks the mechanism directly: a worker writes to its own file, the controller to the main one, and a torn line at the end doesn't sink the read. It fails on the old plugin and passes on the new one, which, given what the tool is for, felt like the least I could do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If several processes append to one file, it works on Linux and macOS and loses data on Windows, quietly and rarely. Give each process its own file, or take a lock.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I drafted this post with an AI assistant. The error, the numbers and the code are from the actual CI run, my machine and the commit: &lt;a href="https://github.com/syntaxixr/receipts/commit/70bb9b2" rel="noopener noreferrer"&gt;https://github.com/syntaxixr/receipts/commit/70bb9b2&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>windows</category>
      <category>pytest</category>
    </item>
    <item>
      <title>A reader picked apart my benchmark. Here is what survived, and what changed</title>
      <dc:creator>syntaxixr</dc:creator>
      <pubDate>Wed, 14 Oct 2026 15:14:00 +0000</pubDate>
      <link>https://dev.to/syntaxixr/a-reader-picked-apart-my-benchmark-here-is-what-survived-and-what-changed-4kj1</link>
      <guid>https://dev.to/syntaxixr/a-reader-picked-apart-my-benchmark-here-is-what-survived-and-what-changed-4kj1</guid>
      <description>&lt;p&gt;Last week I published a study of 181 real code changes, and one sentence in it was wrong. A reader found it by re-adding my numbers. This is what they found, what I changed, and what is still open.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sentence
&lt;/h2&gt;

&lt;p&gt;I'd written that in 10% of agent-written pull requests every test failed on the old code only because the test file imports a name the change adds (so the file can't even load), and that "no maintainer commit in my sample looked like that."&lt;/p&gt;

&lt;p&gt;The reader re-derived my results files before reading the write-up. Every sum held. Then they pointed at a line I'd skipped: the maintainer file lists 9 tests with the verdict WEAK out of 212 (4%), while the agent file lists 79 out of 591 (13%). So the pattern isn't absent on the maintainer side.&lt;/p&gt;

&lt;p&gt;I went and looked. All nine come from one marshmallow feature commit that added a new keyword argument. Its test file loads fine, and the same commit also has tests that do prove the change. So the sentence was about "every test is WEAK", and the fix was to say what the maintainer data actually contains. By change, 14 of 91 agent PRs contain a WEAK test, against 1 of 71 maintainer commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The split that made it more interesting
&lt;/h2&gt;

&lt;p&gt;The reader's next question: does the import pattern show up even when something else already proves the change, or only when nothing does? Splitting the 91 agent PRs by whether they have a proving test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With a proving test: 5 of 80 carry a WEAK test (6.3%, 95% interval 2.7 to 13.8).&lt;/li&gt;
&lt;li&gt;Without one: 9 of 11 (81.8%, interval 52.3 to 94.9).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If WEAK tests were written at the same 6.3% rate everywhere, you'd expect about 0.7 of the 11 to carry one. There are 9. So most of it sits exactly where nothing else proves the change. The reader also pointed out the catch: my change-level rule never calls a change Proven if it carries a WEAK test, so part of that is the aggregation rule read back. The 5 of 80 is the only number that isn't, because those PRs ship a proof and still add a WEAK test. That's an authoring rate, and it's small but not zero.&lt;/p&gt;

&lt;p&gt;I checked their statistics myself (Wilson intervals, Fisher exact tests). They hold. The maintainer side is a different story: 1 of 71 is one event, so it can't tell anyone the base rate, however clean the contrast looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The study doc now says all of the above, with the intervals, and one definition of "weak only" everywhere (my generator and two results files disagreed with the doc).&lt;/li&gt;
&lt;li&gt;Receipts, the tool, now prints how each PROVEN test failed on the old code. A different reader, on Reddit, said "it went red" isn't enough: you want to see that the test reached its assertion, not that it died on an import. The tool knew that and wasn't showing it.&lt;/li&gt;
&lt;li&gt;I wrote down the next step before running it: a few hundred more maintainer commits, as their own row, with one selection rule applied to both sides. The reader predicted the maintainer rate would move up toward the agent rate, because the one maintainer event is itself a churn-shaped commit. That prediction is in the doc now, so whatever comes back gets read against it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;p&gt;Two things. One: re-derive someone's sums before you argue with their sentence, because the sentence is where it's loose. Two: a nice headline number ("10% of agent PRs") can be a fact about your verdict rules as much as about the code, and the only way I found that out was a reader who wasn't invested in the answer.&lt;/p&gt;

&lt;p&gt;The tool is open source and free: &lt;a href="https://github.com/syntaxixr/receipts" rel="noopener noreferrer"&gt;https://github.com/syntaxixr/receipts&lt;/a&gt;. The study with every change linked: &lt;a href="https://github.com/syntaxixr/receipts/blob/main/docs/study.md" rel="noopener noreferrer"&gt;https://github.com/syntaxixr/receipts/blob/main/docs/study.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Heads up: I drafted this post with an AI assistant. The numbers are from the study files in the repo and from the comment thread under the original post.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Claude Code's /goal judge said "done" in 17 of 17 runs. 8 of them were broken.</title>
      <dc:creator>syntaxixr</dc:creator>
      <pubDate>Sun, 04 Oct 2026 12:22:31 +0000</pubDate>
      <link>https://dev.to/syntaxixr/c-1akk</link>
      <guid>https://dev.to/syntaxixr/c-1akk</guid>
      <description>&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%2F9vhrulbxz05y7317jk2a.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%2F9vhrulbxz05y7317jk2a.png" alt="goalpost: makes Claude Code /goal actually finish the job" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I like &lt;code&gt;/goal&lt;/code&gt; in Claude Code. You say what "done" looks like and Claude keeps working until it gets there. What bothered me is who decides it got there.&lt;/p&gt;

&lt;p&gt;After every turn a small model (Haiku) reads the conversation and answers "met", "not yet" or "impossible". I tested what that judge can see. It never runs a command and never opens a file. If the agent writes "All done, tests pass!", the judge can just believe it.&lt;/p&gt;

&lt;p&gt;So I measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark
&lt;/h2&gt;

&lt;p&gt;Four small tasks: a ledger, a text toolkit, a job queue and a spreadsheet engine. Each one has a hidden grader that the agent never sees. I ran Sonnet 5.5 and Haiku 4.5 on them, with plain &lt;code&gt;/goal&lt;/code&gt; and with my plugin, and graded every run three times. 33 valid runs in total.&lt;/p&gt;

&lt;p&gt;The judge said "met" in all 17 plain runs. Eight of those were broken: one Sonnet run and seven Haiku runs. The worst one was a Haiku run on the spreadsheet task that passed 4% of the hidden checks, and &lt;code&gt;/goal&lt;/code&gt; still called it done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/syntaxixr/goalpost" rel="noopener noreferrer"&gt;goalpost&lt;/a&gt; is a Claude Code plugin made of hooks. You keep typing &lt;code&gt;/goal&lt;/code&gt; like before. Behind it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The goal becomes a spec with acceptance criteria. Each criterion has a command that exits 0 only when it holds.&lt;/li&gt;
&lt;li&gt;Claude can't stop while a criterion has no passing check, and a check only counts if it ran after the last edit.&lt;/li&gt;
&lt;li&gt;Tests that existed when the goal started are read-only, so they can't be edited to fit the code.&lt;/li&gt;
&lt;li&gt;When everything passes, a fresh auditor agent re-runs the checks and looks for stubs, skipped tests and hardcoded answers.&lt;/li&gt;
&lt;li&gt;After context compaction the spec and the progress notes come back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's hooks, not a prompt, so the model can't talk its way past it.&lt;/p&gt;

&lt;p&gt;One example from the benchmark: in a Sonnet run on the spreadsheet task, the agent changed the expected values in four of its own tests after the spec was frozen, for example &lt;code&gt;AND(A3,A1)&lt;/code&gt; from &lt;code&gt;#VALUE!&lt;/code&gt; to &lt;code&gt;TRUE&lt;/code&gt;. The auditor caught it ("expectation changed after the freeze, undeclared"), the agent explained the change, and the run finished with all 73 hidden checks green. A plain run has nobody looking at that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest numbers
&lt;/h2&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;plain &lt;code&gt;/goal&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;with goalpost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5, runs fully correct&lt;/td&gt;
&lt;td&gt;8 of 9&lt;/td&gt;
&lt;td&gt;9 of 9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 4.5, hidden checks passed (mean)&lt;/td&gt;
&lt;td&gt;79.8%&lt;/td&gt;
&lt;td&gt;89.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 4.5, runs fully correct&lt;/td&gt;
&lt;td&gt;1 of 8&lt;/td&gt;
&lt;td&gt;1 of 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time per run (Sonnet / Haiku)&lt;/td&gt;
&lt;td&gt;2.7 / 5.7 min&lt;/td&gt;
&lt;td&gt;5.2 / 15.1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per run (Sonnet / Haiku)&lt;/td&gt;
&lt;td&gt;$0.46 / $0.64&lt;/td&gt;
&lt;td&gt;$0.91 / $1.25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sonnet barely needed help on these tasks, and one extra perfect run is within noise. Haiku improved but it didn't become reliable: only one of its seven runs with goalpost was fully correct, and the auditor is a Haiku too, so it can wave things through. And it costs about twice the time and money. I'd use it for goals that matter, and turn it off for small ones (&lt;code&gt;/goalpost:off&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The runs are small because my account's rate limit cut several of them. The full report lists every run I threw out.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude plugin marketplace add syntaxixr/goalpost
claude plugin install goalpost@goalpost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open a new Claude Code session and type &lt;code&gt;/goal&lt;/code&gt; as usual. It needs Node.js 18 or newer. The repo has the benchmark, the notes on how &lt;code&gt;/goal&lt;/code&gt; works under the hood, and a 90-second video of two real runs.&lt;/p&gt;

&lt;p&gt;If you've seen &lt;code&gt;/goal&lt;/code&gt; end too early on your own work, I'd like to hear about it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Heads up: I drafted this post with an AI assistant. The numbers come from the benchmark report in the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I reverted the fix in 181 real changes to see if the tests would notice</title>
      <dc:creator>syntaxixr</dc:creator>
      <pubDate>Wed, 30 Sep 2026 12:05:24 +0000</pubDate>
      <link>https://dev.to/syntaxixr/i-reverted-the-fix-in-181-real-changes-to-see-if-the-tests-would-notice-2e3</link>
      <guid>https://dev.to/syntaxixr/i-reverted-the-fix-in-181-real-changes-to-see-if-the-tests-would-notice-2e3</guid>
      <description>&lt;p&gt;I keep running into the same thing with coding agents. The agent fixes a bug, adds a test, CI goes green, and it says "done". But green only means the test passes. It doesn't mean the test would have failed before the fix. And if it wouldn't, it checks nothing about the bug, however green it is.&lt;/p&gt;

&lt;p&gt;The old-school way to find out is boring: revert the fix and run the test again. So I wrote a tool that does exactly that for every test in a change, and pointed it at the real history of 17 open-source projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the check works
&lt;/h2&gt;

&lt;p&gt;For each test a change adds or edits, it does two runs. First the test runs with the change. Then only the changed source files go back to the base branch (tests, dependencies and config stay new) and the same test runs again.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fails without the fix, passes with it: &lt;strong&gt;PROVEN&lt;/strong&gt;. This is what you want.&lt;/li&gt;
&lt;li&gt;Passes both ways: &lt;strong&gt;THEATER&lt;/strong&gt;. It proves nothing about this change.&lt;/li&gt;
&lt;li&gt;Fails on the old code only because it imports something that didn't exist yet: &lt;strong&gt;WEAK&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Passes both ways but sits next to a test that does prove the change: &lt;strong&gt;GUARD&lt;/strong&gt;. That's fine, it's the "did I break the neighboring case" test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiua4h0ian6mc1q5s37bq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiua4h0ian6mc1q5s37bq.gif" alt="receipts check: THEATER, then PROVEN after a real test is added" width="799" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I found
&lt;/h2&gt;

&lt;p&gt;I took two samples. 81 fix commits from the maintainers of twelve libraries (click, sqlparse, marshmallow, dateutil, more-itertools and others), and 100 pull requests that carry a coding agent's fingerprint, from five repos that are full of agent work: Claude Agent SDK, OpenAI Agents SDK, the MCP Python SDK, fastmcp and simonw/llm. 87 of those 100 were Claude Code.&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;Proven&lt;/th&gt;
&lt;th&gt;Weak only&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Maintainer fix commits&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent pull requests&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Good news first: most tests do their job. These are well-run projects, some of them run by the companies that build the agents.&lt;/p&gt;

&lt;p&gt;The difference is that WEAK column. In 10% of the agent PRs, every test failed on the old code for the same reason: the test file imports, at the top, a name that the change adds. On the old code that import breaks, the whole file can't load, and every test in it "fails". It looks like proof, but none of those tests ever ran against the old behavior.&lt;/p&gt;

&lt;p&gt;One example is &lt;a href="https://github.com/anthropics/claude-agent-sdk-python/pull/1016" rel="noopener noreferrer"&gt;anthropics/claude-agent-sdk-python#1016&lt;/a&gt;. The test file gets two new imports at the top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;claude_agent_sdk.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TERMINAL_TASK_STATUSES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;TaskUpdatedMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the old code neither name exists, so all 10 changed tests die at import, including one that was already there. The fix may well be right. The tests just can't show it. The cheap cure is to import new names inside the tests that need them, so the rest of the file still runs on the old code.&lt;/p&gt;

&lt;p&gt;No maintainer commit in my sample looked like that.&lt;/p&gt;

&lt;h2&gt;
  
  
  THEATER is rare
&lt;/h2&gt;

&lt;p&gt;Only 7 of the 162 changes I could judge were unproven, and every one had a reason. Type-only fixes (runtime tests can't see types). A Windows newline fix tested on Linux, where the bug doesn't exist. A dateutil &lt;code&gt;__repr__&lt;/code&gt; fix whose new output matched what the inherited default already printed. A maintenance commit that happened to mention an issue number. So when the tool says THEATER, there's usually something worth a look.&lt;/p&gt;

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

&lt;p&gt;In any git repo, on a branch with a fix and its test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx github:syntaxixr/receipts check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's deterministic, with no LLM and no API keys, and it runs your own pytest, vitest or jest. There's also a GitHub Action that keeps one comment on the PR, and a skill so the agent checks its own tests before it says it's done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Claude Code
/plugin marketplace add syntaxixr/receipts
/plugin install receipts-check@receipts

# Codex, Cursor and other agents
npx skills add syntaxixr/receipts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The fine print
&lt;/h2&gt;

&lt;p&gt;The samples are small and not random, so the numbers describe those projects, not the whole ecosystem. "Agent-authored" only means an agent's fingerprint is in the commits, and humans steer those agents. "Judged" means at least one test ran on both sides. The rest failed for environment reasons, since I installed each project once at its current tip. PROVEN means the test notices the change, not that the change is correct. Python and JS/TS only for now.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/syntaxixr/receipts" rel="noopener noreferrer"&gt;https://github.com/syntaxixr/receipts&lt;/a&gt;. Every change with a link and the method: &lt;a href="https://github.com/syntaxixr/receipts/blob/main/docs/study.md" rel="noopener noreferrer"&gt;https://github.com/syntaxixr/receipts/blob/main/docs/study.md&lt;/a&gt;. Data: &lt;a href="https://huggingface.co/datasets/syntaxixr/receipts-study" rel="noopener noreferrer"&gt;https://huggingface.co/datasets/syntaxixr/receipts-study&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it says something wrong about your code, I'd like to hear about it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Heads up: I drafted this post with an AI assistant. The numbers come from the study in the repo, and every change is linked there so you can check them yourself.&lt;/em&gt;&lt;/p&gt;

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