<?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://dev.to/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://dev.to/heinrichneb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/heinrichneb"/>
    <language>en</language>
    <item>
      <title>I tested Claude Code's memory against mine. They are not doing the same job.</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Tue, 01 Sep 2026 20:40:47 +0000</pubDate>
      <link>https://dev.to/heinrichneb/i-tested-claude-codes-memory-against-mine-they-are-not-doing-the-same-job-35jb</link>
      <guid>https://dev.to/heinrichneb/i-tested-claude-codes-memory-against-mine-they-are-not-doing-the-same-job-35jb</guid>
      <description>&lt;p&gt;&lt;em&gt;Proof over claim&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last time I described &lt;a href="https://dev.to/heinrichneb/the-best-argument-against-my-mcp-server-came-from-anthropic-p1m"&gt;the hour a platform shipped the feature I had spent months building&lt;/a&gt;. This is the part where I ran the test instead of the emotions, and found two things that were never competing.&lt;/p&gt;

&lt;p&gt;One number up front, because it qualifies everything below: this is still one user. Every figure here comes from my corpus, my questions, my four servers. The method transfers. Whether the result does, I cannot tell you yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same test, two different answers
&lt;/h2&gt;

&lt;p&gt;The test from last time is one question: teach it a fact only true in your world, close everything, come back in a fresh session, and count how often the fact comes back.&lt;/p&gt;

&lt;p&gt;Both passed. That was the first surprise, and it is worth saying clearly: the built-in memory works. This is not a post about a competitor being bad.&lt;/p&gt;

&lt;p&gt;The difference showed up when I changed one thing in the test. I asked the fact from a different editor.&lt;/p&gt;

&lt;p&gt;And then again from a different machine. And then I asked a colleague to ask it.&lt;/p&gt;

&lt;p&gt;That is where the two answers stopped matching, and it had nothing to do with quality. It had to do with what each thing considers its own boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session memory and project memory are different products
&lt;/h2&gt;

&lt;p&gt;Here is the distinction I could not put into words on that bad evening, and it took a test to produce it.&lt;/p&gt;

&lt;p&gt;A vendor's memory is bound to the vendor's harness. It makes one assistant continuous with itself. That is genuinely valuable and it is what most people mean when they ask for memory.&lt;/p&gt;

&lt;p&gt;What I had built is bound to the repository instead. It makes the knowledge about a codebase continuous — across editors, across machines, across people, across model upgrades.&lt;/p&gt;

&lt;p&gt;Those are not two implementations of one feature. They are answers to two different questions. "What did I just say?" and "what does this project know?" only look similar until you switch tools.&lt;/p&gt;

&lt;p&gt;The clearest way to see it: when someone leaves your team, a session memory leaves with them. A project memory does not, because it was never theirs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boundary a similarity search cannot cross
&lt;/h2&gt;

&lt;p&gt;The four boundaries above are about &lt;em&gt;where&lt;/em&gt; memory lives. There is a fifth one, and it is about how you find anything in it — I only understood it because someone else wrote it down.&lt;/p&gt;

&lt;p&gt;Think about the two entries that matter most together: an error, and the fix that was found three weeks later. Write them out.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;deploy hangs at "Build image", worker log says nothing&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;the runner disk was full; docker prune and restart the service&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those two share almost no vocabulary. One is a symptom, one is a cause; one is about a build step, one is about a disk. In embedding space they sit far apart — not slightly, structurally. And they are the single most valuable pair in the whole store, because together they are the answer and apart they are two anecdotes.&lt;/p&gt;

&lt;p&gt;A similarity search cannot connect them. Not because the embeddings are bad. Because &lt;em&gt;semantic closeness and causal connection are different relations&lt;/em&gt;, and only one of them is what a vector index measures.&lt;/p&gt;

&lt;p&gt;That is why our store keeps a causal path beside the similarity one — which entry led to which, which contradicts which. I had treated that as a secondary feature for a long time. It is the part that similarity cannot do, and I needed an outside article to see it.&lt;/p&gt;

&lt;p&gt;This also cuts against my own product, so I will say it plainly: if what you need is "find me the thing that sounds like this", a vector search over a file does that, and the extra machinery earns nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The things my own tool did that I had not noticed
&lt;/h2&gt;

&lt;p&gt;This is the embarrassing part, and it is the reason I am writing it down. I had built features I did not use, because my own use case was one person and four servers.&lt;/p&gt;

&lt;p&gt;It can hand knowledge between people. I had built the sharing path months earlier for a technical reason and never once used it, because I work alone on this. For a team it is the entire point: one person debugs a thing at 2 a.m., everyone else inherits the reason.&lt;/p&gt;

&lt;p&gt;It can answer why, not just what. Every entry carries what worked and what failed. I had been reading only the first field for months. The second one is where the expensive knowledge lives — the approach that looked right and was not.&lt;/p&gt;

&lt;p&gt;It can be asked from things that are not editors. Because it speaks MCP and HTTP, our own operations dashboard queries it: a question box that answers from the event stream plus the stored lessons. I built that as a side project and it turned out to be a second product surface.&lt;/p&gt;

&lt;p&gt;And it does not care which model you use. The lessons written by one model are read by the next one. I had treated that as an implementation detail. It is the reason the store survives an upgrade cycle that rewrites everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the vendor's version is simply better
&lt;/h2&gt;

&lt;p&gt;A comparison that only finds advantages is an advertisement, so here is the other direction, and I mean it.&lt;/p&gt;

&lt;p&gt;Setup: theirs is zero. It exists the moment you install. Mine needed a decision, an account and configuration — and measuring that gap honestly is what pushed us to a trial that needs no sign-up at all.&lt;/p&gt;

&lt;p&gt;Depth inside one conversation: theirs sits in the harness and sees everything. Anything from the outside sees what it is told, and that is a real ceiling, not a temporary one.&lt;/p&gt;

&lt;p&gt;Trust: the vendor already holds your code. Handing the same knowledge to a second party is an extra decision, and "it is hosted in the EU" is an answer, not a dismissal of the question.&lt;/p&gt;

&lt;p&gt;If your work lives in one assistant, on one machine, alone, the built-in memory is the right choice and I would tell you so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure it yourself instead of believing either of us
&lt;/h2&gt;

&lt;p&gt;The test that produced all of this is four variations of one question, and the variations are the whole trick. Same fact, different boundary.&lt;/p&gt;

&lt;p&gt;Run it in the tool you taught the fact to. Then in a second editor. Then on a second machine. Then have someone else ask.&lt;/p&gt;

&lt;p&gt;Each of those four is a boundary a memory either crosses or does not, and no announcement will tell you which — the crossing is the thing that got designed, and it rarely shows up in release notes.&lt;/p&gt;

&lt;p&gt;Write the four numbers down before you form an opinion. The shape of the four tells you which product you are holding.&lt;/p&gt;

&lt;p&gt;The harness, extended from last time:&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;# Four boundaries, one fact. The SHAPE of the four numbers is the answer.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="nv"&gt;FACT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"which port the staging database listens 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;# what you stored, written down BEFORE you ask&lt;/span&gt;

ask&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;  &lt;span class="c"&gt;# ask &amp;lt;label&amp;gt; &amp;lt;command...&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="o"&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="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift
  local &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;1 2 3 4 5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &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;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qF&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="o"&gt;&amp;amp;&amp;amp;&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;done
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%-22s %d/5\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="s2"&gt;"&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="o"&gt;}&lt;/span&gt;

ask &lt;span class="s2"&gt;"same tool"&lt;/span&gt;      your-assistant &lt;span class="nt"&gt;--new-session&lt;/span&gt; &lt;span class="nt"&gt;--ask&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FACT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
ask &lt;span class="s2"&gt;"other editor"&lt;/span&gt;   other-editor-cli &lt;span class="nt"&gt;--ask&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FACT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
ask &lt;span class="s2"&gt;"other machine"&lt;/span&gt;  ssh other-box  your-assistant &lt;span class="nt"&gt;--ask&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FACT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# The fourth one is not scriptable, and that is the point:&lt;/span&gt;
&lt;span class="c"&gt;# ask a colleague to run the same question on their own machine.&lt;/span&gt;
&lt;span class="c"&gt;# ask "other person"  ...&lt;/span&gt;

&lt;span class="c"&gt;# 5/5 0/5 0/5 0/5  -&amp;gt; session memory. Continuous with itself.&lt;/span&gt;
&lt;span class="c"&gt;# 5/5 5/5 5/5 5/5  -&amp;gt; project memory. Continuous with the repository.&lt;/span&gt;
&lt;span class="c"&gt;# Neither is wrong. They are answers to different questions, and you now&lt;/span&gt;
&lt;span class="c"&gt;# know which one you have.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One warning from our own numbers: do not measure recall quality by whether the answer sounds right. We once had a stored entry containing the exact address we needed, displayed at session start, and made the mistake anyway — the preview cut off at a hundred characters and the address sat at character three hundred and twenty-three. Delivery is a separate measurement from storage, and it is the one that decides whether any of this pays.&lt;/p&gt;

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

&lt;p&gt;Before: a platform ships something adjacent to your work and you decide, from the announcement, whether you are finished. Half the time you are wrong in the pessimistic direction, which costs you the thing you were building.&lt;/p&gt;

&lt;p&gt;After: you run four variations of one test, get four numbers, and find out whether you were building the same product or a neighbouring one. It takes an afternoon and it replaces a week of dread.&lt;/p&gt;

&lt;p&gt;The thing I actually learned is not about competition. It is that I had never described my own product, and a platform release forced me to — which turned out to be the most useful thing anyone did for it all year.&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. Every assistant reads the same memory, and every lesson carries the name of whoever learned it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;30 seconds, no account&lt;/strong&gt; — &lt;code&gt;npx @cachly-dev/mcp-server@latest demo&lt;/code&gt; in any git repo. It reads your log locally and prints what an assistant would already know about the project. After npx fetches the package, the command makes no network calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code plugin&lt;/strong&gt; — &lt;code&gt;/plugin marketplace add cachly-dev/cachly-mcp&lt;/code&gt;, then &lt;code&gt;/plugin install cachly-brain@cachly&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5 minutes, free tier&lt;/strong&gt; — &lt;code&gt;npx @cachly-dev/mcp-server@latest autopilot&lt;/code&gt; writes the MCP configuration for whichever assistant you use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Or from the web&lt;/strong&gt; — &lt;a href="https://cachly.dev?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_content=not-the-same-job-at-all" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt; · free tier, German servers, no credit card.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>productivity</category>
      <category>llm</category>
    </item>
    <item>
      <title>Your AI Has a Reviewer. Has Anyone Ever Seen It Say No?</title>
      <dc:creator>Heinrich Neb</dc:creator>
      <pubDate>Thu, 27 Aug 2026 04:25:16 +0000</pubDate>
      <link>https://dev.to/heinrichneb/your-ai-has-a-reviewer-has-anyone-ever-seen-it-say-no-4ja8</link>
      <guid>https://dev.to/heinrichneb/your-ai-has-a-reviewer-has-anyone-ever-seen-it-say-no-4ja8</guid>
      <description>&lt;p&gt;&lt;em&gt;Two weeks ago I counted 204 guards in my repos and found that 89 % had never been shown they can fail. I fixed that for a batch of them. Then tonight I filed a data-loss report about 1,000 files that were never lost — and nobody, including me, had a number to check it against.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last time I wrote here, the number was 204 and 11 %: of the automated checks in my repositories that draw a conclusion, only about one in nine could prove it was capable of failing. &lt;a href="https://dev.to/dev_michael"&gt;@dev_michael&lt;/a&gt;'s "AI didn't make me a worse coder, it made me a worse reviewer" is what started that count.&lt;/p&gt;

&lt;p&gt;So I did the obvious follow-up work: I made a batch of guards prove they can fail. That fix is real and I'll describe it below, because it costs an afternoon and it works.&lt;/p&gt;

&lt;p&gt;It also wasn't enough, and I found out the embarrassing way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part one: the fix that works
&lt;/h2&gt;

&lt;p&gt;Every reviewer — a hand-written check, an LLM judge, a second agent grading the first — gets &lt;strong&gt;one known-bad case wired through the live path&lt;/strong&gt;. Not a unit test beside the pipeline. The same entry point real work uses.&lt;/p&gt;

&lt;p&gt;Our benchmark harness runs three gates per case, and a case that misses any of them does not run at all:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unsolved state → must go RED.&lt;/strong&gt; The check must reject the broken repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solved state → must go GREEN.&lt;/strong&gt; It must accept the fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known-bad → must go RED again.&lt;/strong&gt; We re-plant the original mistake deliberately; if the check still passes, it never understood the bug.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Gate three is the one that earns its keep. It has caught real breakage — including, twice, in guards written &lt;em&gt;that same week&lt;/em&gt; to catch exactly this class of problem.&lt;/p&gt;

&lt;p&gt;This week I pushed it further, because a harness is only as good as the cases in it, and inventing cases is where everyone runs out of imagination. &lt;a href="https://dev.to/shreyasht"&gt;@shreyasht&lt;/a&gt; killed his own token-optimization project after discovering his best result — 97 % savings — came from the run that did &lt;em&gt;no work at all&lt;/em&gt;: the agent asked a clarifying question, stopped, and the dashboard crowned it. A metric with no way to punish doing nothing eventually rewards it. His fix is one sentence: measure per &lt;em&gt;solved&lt;/em&gt; task, never per task.&lt;/p&gt;

&lt;p&gt;The imagination problem has a cheap answer, and it was sitting in our database: &lt;strong&gt;the incident history is the test suite.&lt;/strong&gt; We took ten real recorded failures — the kind with a "what went wrong" field written by someone annoyed at the time — and had a model punch each one into a runnable check. Ten lessons in, ten runnable cases out, thirty gates, all thirty passed. Zero discarded.&lt;/p&gt;

&lt;p&gt;One thing that surprised me: the first attempt failed the internals filter, and it failed because the &lt;em&gt;example&lt;/em&gt; we handed the model contained a product name in a comment. The model copied it faithfully. The filter caught it. A guard I had written to be paranoid about human sloppiness caught a machine being obedient instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/wrobeltomasz"&gt;@wrobeltomasz&lt;/a&gt; described this same discipline independently in a comment thread while this draft sat unpublished: identify the checks, then run them "in simulation mode to confirm that they can indeed respond to invalid input" — moving verification "from statistics in the README to actual system resilience." Two people who have never met, same gate, same reason. That usually means the pattern is real and not a personal quirk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part two: the half I had not fixed
&lt;/h2&gt;

&lt;p&gt;Here is what all of the above proves: on the day I wired it, that reviewer &lt;em&gt;could&lt;/em&gt; say no.&lt;/p&gt;

&lt;p&gt;It says nothing whatsoever about today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/mk023"&gt;@mk023&lt;/a&gt; said the correcting sentence in a thread under my last post, and gave me permission to quote it here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Don't just test that the guard can fail — test that it is still guarding the thing you think it is guarding."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That completes the model. A green check can be lying in three orthogonal ways: &lt;strong&gt;falsifiability&lt;/strong&gt; — can it go red at all; &lt;strong&gt;liveness&lt;/strong&gt; — can it go red against today's system; and &lt;strong&gt;aim&lt;/strong&gt; — is the boundary it defends still the one that matters. Part one only bought me the first. Marco's sentence is the other two, and the rest of this post is me finding out the hard way that he was right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/james_anderson_h"&gt;@james_anderson_h&lt;/a&gt;, in a thread about AI workspaces that ship with "guardrails — approval gates, an audit trail, a second agent that reviews the first," put the gap better than my question had: most tools hand you the audit log, &lt;strong&gt;not proof the veto still fires&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A checker nobody has ever seen fail is indistinguishable from a checker that approves everything. The two produce identical logs right up until the day the rubber-stamp waves through the thing that hurts you."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My answer to him was a small thing I now think is the most useful idea in this post: &lt;strong&gt;the veto heartbeat.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Surface one date. &lt;em&gt;"Last time this reviewer refused something: 2 days ago."&lt;/em&gt; First-class, visible, right next to the uptime number. The known-bad case runs on a schedule; if the last-refusal date ages past that schedule, &lt;strong&gt;staleness itself is the alarm&lt;/strong&gt;. No log-diving, no trusting a vendor's dashboard, no archaeology. One date anyone can read in one glance — and the absence of fresh red finally &lt;em&gt;looks like&lt;/em&gt; what it is.&lt;/p&gt;

&lt;p&gt;James named the principle underneath it better than I had:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Silence and health look identical unless you deliberately build a state for 'not verified lately'."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the whole bug in one line. Most systems model &lt;strong&gt;pass&lt;/strong&gt; and &lt;strong&gt;fail&lt;/strong&gt; and nothing else. A veto that quietly died therefore reads exactly like a veto that simply had nothing to refuse. The heartbeat is that missing third state, worn on the outside where an auditor can see it without your permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I was the punchline, tonight
&lt;/h2&gt;

&lt;p&gt;I was going to publish the section above as the ending. Then I spent an evening proving the point against myself, and the story is better than the theory.&lt;/p&gt;

&lt;p&gt;I went to check on a long-running harvest job spread across two machines. I connected to one, looked for the working directory, found nothing, looked for the process, found nothing. So I reported it: the data is gone, roughly a thousand collected items lost, and I filed a ticket saying so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I had connected to the wrong machine.&lt;/strong&gt; The numbering I used to pick it does not mean what I assumed it means — a fact that is written down in my own project instructions, in bold, with an example. I had read it before. I did it anyway.&lt;/p&gt;

&lt;p&gt;Nothing was lost. The harvest was sitting exactly where it should be, and a second job was quietly running on it at that very moment.&lt;/p&gt;

&lt;p&gt;Now the part that matters, because "I made a dumb mistake" is not an article. &lt;strong&gt;Why did the false report survive as long as it did?&lt;/strong&gt; Because there was nothing to contradict it. No dashboard, no counter, no file with three numbers in it. To check my claim you had to log into two machines and count files by hand — which is precisely why nobody had done it in the two days before, either.&lt;/p&gt;

&lt;p&gt;And when I finally &lt;em&gt;did&lt;/em&gt; count, the numbers exposed something nobody had noticed: the two machines had been working the same list from opposite ends, had met in the middle days ago, and had since re-collected &lt;strong&gt;332 items both already had.&lt;/strong&gt; Not broken. Not alarming. Just quietly wasteful, in a system with no visible number to be quietly wasteful in front of.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two failures have exactly one cause
&lt;/h2&gt;

&lt;p&gt;A green check nobody can question and a red alarm nobody can question are the &lt;em&gt;same&lt;/em&gt; bug wearing different clothes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A guard that has silently stopped being able to fail produces &lt;strong&gt;unfalsifiable green&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A monitor with no visible state produces &lt;strong&gt;unfalsifiable red&lt;/strong&gt; — my false report, which survived only because contradicting it required work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both come from the same missing thing: &lt;strong&gt;a number that a stranger can read without your cooperation.&lt;/strong&gt; The veto heartbeat is that number for a reviewer. Three counters in a file is that number for a background job. Neither is clever. Both are absent almost everywhere, including — until this week — in a codebase belonging to someone who writes about this for a living.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/bert_sk_shim_cb93b1"&gt;@bert_sk_shim_cb93b1&lt;/a&gt; hit the same class from the opposite side while this was being written, and named the part I had missed. His login check queried an endpoint scoped to a different auth method, so it returned "no username" no matter what — &lt;strong&gt;always negative&lt;/strong&gt; instead of always green. Then the sentence that reframed this whole post for me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A red result tends to get remediated rather than investigated, so I had someone go log in again, which was unnecessary, and if the timing had been a bit different I'd have credited that as the fix and kept the broken check."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the asymmetry. Green invites complacency, but red invites &lt;em&gt;action&lt;/em&gt; — and action feels like resolution. A broken always-red check gets a workaround performed in front of it, and the workaround gets credited. He very nearly filed an unnecessary login as the fix and kept the instrument that lied to him. &lt;strong&gt;Always-negative hides better than always-green&lt;/strong&gt;, and it is the direction almost nobody watches.&lt;/p&gt;

&lt;p&gt;His fix generalizes further than either of our stories: separate &lt;strong&gt;presence&lt;/strong&gt; from &lt;strong&gt;value&lt;/strong&gt;. The operational version I have landed on since is one line — &lt;em&gt;print what you read before you print what you concluded.&lt;/em&gt; "No username at /whoami (auth scheme B)" is a bug report. "Not logged in" is a rumour with a status code. My false alarm would have died in thirty seconds if my own check had said &lt;em&gt;which machine answered&lt;/em&gt; instead of only &lt;em&gt;what I concluded about it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/pm25coder"&gt;@pm25coder&lt;/a&gt; shipped the same move at a different layer while I was writing this: their harness now appends every silent-safety-net event to an append-only file that survives restarts and log rotation, so "did the safety net ever misfire?" became a lookup instead of archaeology. Evidence that outlives the process that produced it. That is the whole idea, and it is worth stealing.&lt;/p&gt;

&lt;p&gt;And &lt;a href="https://dev.to/johngreen"&gt;John Green&lt;/a&gt; has been running the harshest version of this discipline on model evaluations: he reran his exam five times and watched his published winner evaporate — the behaviour that decided the ranking reproduced zero times out of five. His line stays with me: &lt;em&gt;"The exam caught its author again — not in the answer key, not in the grader — in how confidently I read a single run."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Copy that
&lt;/h2&gt;

&lt;p&gt;Paste this next to any pipeline that claims to have guardrails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REVIEWER CHECKLIST — "exists" is not "works"
[ ] One known-bad case wired through the LIVE path (not a side test)
[ ] It runs on a schedule, not once at setup
[ ] Its last RED has a visible timestamp ("last refusal: …")
[ ] Timestamp older than the schedule =&amp;gt; alarm, automatically
[ ] The metric it guards punishes doing nothing (per SOLVED unit)
[ ] Someone has seen every terminal state fire at least once

MONITOR CHECKLIST — an alarm needs a number too
[ ] Three counters, readable without logging into anything
[ ] The identity of the thing measured is IN the output
    (which machine, which provider, which run — not inferred)
[ ] Silence has its own state, distinct from "healthy"
[ ] Every verdict prints the VALUE it read, not just the conclusion
[ ] Check the always-negative direction too: a check that can only
    say no is as broken as one that can only say yes — and it hides
    better, because red gets remediated instead of investigated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The 60-second version, if you do nothing else:&lt;/strong&gt; open the oldest green check in your pipeline and ask when it last went red. If the answer needs a log search, you have found one. If you cannot answer it at all, you have found a bigger one.&lt;/p&gt;

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

&lt;p&gt;The three gates prove a check &lt;em&gt;could&lt;/em&gt; fail on a case someone thought of. They say nothing about the cases nobody thought of, and harvesting your own incident history — which I recommend — has a bias baked in: those cases come from the same system that produced the failures, so they may be systematically easier than reality. If your harvested cases pass at a noticeably higher rate than your invented ones, that is a finding, not a win. Report the two separately.&lt;/p&gt;

&lt;p&gt;The heartbeat has a failure mode too: a scheduled known-bad case can become a ritual that always passes, and then the fresh date is reassuring rather than informative. The honest mitigation is rotation — change the known-bad case periodically — and I am not going to pretend I have a clean answer for how often.&lt;/p&gt;

&lt;p&gt;And the counters are not free. Three numbers per job is a small amount of code and a permanent amount of maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I'll leave you with
&lt;/h2&gt;

&lt;p&gt;Find the oldest green checkmark in your pipeline — the linter gate, the LLM judge, the approval step that has passed everything for months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When did it last say no? Do you know? Can you find out without asking the person who built it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "I would have to check the logs," you do not have a guardrail. You have a rubber stamp with excellent posture.&lt;/p&gt;

&lt;p&gt;I would genuinely like to hear two things in the comments: &lt;strong&gt;the age of the oldest never-red green in your system&lt;/strong&gt;, and — the one I now care about more — &lt;strong&gt;the last time an alarm in your system turned out to be wrong, and how long it took anyone to prove it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mine took an evening, and only because I finally counted.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Quoted with thanks: &lt;a href="https://dev.to/mk023"&gt;@mk023&lt;/a&gt;, whose one sentence is the whole of part two, &lt;a href="https://dev.to/shreyasht"&gt;@shreyasht&lt;/a&gt;'s token-stack post-mortem, &lt;a href="https://dev.to/johngreen"&gt;John Green&lt;/a&gt;'s AI-verification series, &lt;a href="https://dev.to/james_anderson_h"&gt;@james_anderson_h&lt;/a&gt;'s workspace thread, and &lt;a href="https://dev.to/wrobeltomasz"&gt;@wrobeltomasz&lt;/a&gt;, &lt;a href="https://dev.to/pm25coder"&gt;@pm25coder&lt;/a&gt; and &lt;a href="https://dev.to/bert_sk_shim_cb93b1"&gt;@bert_sk_shim_cb93b1&lt;/a&gt; in the comments — Marco's and James' lines with their explicit blessing. Bert's always-negative direction is the half of this post I would not have found on my own.&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;The incident-history-as-test-suite trick in this post runs directly on that memory: the lessons were already there, written at the moment someone was annoyed enough to record them.&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=has-anyone-seen-your-reviewer-say-no" rel="noopener noreferrer"&gt;cachly.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>discuss</category>
      <category>programming</category>
    </item>
    <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://dev.to/heinrichneb/ai-promoted-every-developer-to-reviewer-nobody-tested-the-reviewer-m4h</link>
      <guid>https://dev.to/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://dev.to/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://dev.to/heinrichneb/what-does-your-ai-assistant-remember-from-yesterday-17b8</link>
      <guid>https://dev.to/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://dev.to/heinrichneb/the-best-argument-against-my-mcp-server-came-from-anthropic-p1m</link>
      <guid>https://dev.to/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://dev.to/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://dev.to/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://dev.to/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://dev.to/abhiix0"&gt;@abhiix0&lt;/a&gt;) and &lt;strong&gt;Suraj Suradkar&lt;/strong&gt; (&lt;a class="mentioned-user" href="https://dev.to/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://dev.to/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://dev.to/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://dev.to/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://dev.to/heinrichneb/i-built-an-mcp-memory-server-for-one-user-me-for-six-weeks-30fh</link>
      <guid>https://dev.to/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://dev.to/heinrichneb/four-green-ci-checks-all-four-blind-18b</link>
      <guid>https://dev.to/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://dev.to/heinrichneb/one-repo-one-day-4-ci-guards-that-were-green-and-blind-oge"&gt;https://dev.to/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://dev.to/heinrichneb/one-repo-one-day-4-ci-guards-that-were-green-and-blind-oge</link>
      <guid>https://dev.to/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://dev.to/heinrichneb/my-best-looking-github-actions-run-shipped-zero-installs-1ee8</link>
      <guid>https://dev.to/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://dev.to/heinrichneb/your-ci-is-not-flaky-it-fails-every-7-days-4ljd</link>
      <guid>https://dev.to/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://dev.to/heinrichneb/has-your-github-actions-gate-ever-said-no-dj2</link>
      <guid>https://dev.to/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://dev.to/heinrichneb/my-own-tooling-rejected-me-4-times-every-one-was-the-same-mistake-2lld</link>
      <guid>https://dev.to/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>
  </channel>
</rss>
