<?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: Dan Mercede</title>
    <description>The latest articles on DEV Community by Dan Mercede (@danmercede).</description>
    <link>https://dev.to/danmercede</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%2F3923648%2F1e0611d1-7f32-4988-be5c-ff9c989aee10.png</url>
      <title>DEV Community: Dan Mercede</title>
      <link>https://dev.to/danmercede</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danmercede"/>
    <language>en</language>
    <item>
      <title>The Recon Refuted the Task: Building Infrastructure With an Agent You Can Trust</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Sun, 12 Jul 2026 04:57:27 +0000</pubDate>
      <link>https://dev.to/danmercede/the-recon-refuted-the-task-building-infrastructure-with-an-agent-you-can-trust-2o07</link>
      <guid>https://dev.to/danmercede/the-recon-refuted-the-task-building-infrastructure-with-an-agent-you-can-trust-2o07</guid>
      <description>&lt;p&gt;I asked an agent to build a monitor: something to watch the background jobs across my servers and tell me when one broke. It came back and told me not to build it.&lt;/p&gt;

&lt;p&gt;Not out of laziness. It had spent its first hour running probes against the live machines instead of writing code, and the probes said the thing I asked for already existed five times over. One platform watched the hosts. A container metrics collector watched the containers. The init system watched its own units. Two more scripts watched the edges. What I was actually missing was not another detector. It was the layer that reads five noisy signals and says "these three are the same incident, here is the likely cause, here is the runbook that fixes it."&lt;/p&gt;

&lt;p&gt;That refusal was the single most valuable thing the agent did on the whole project. A detector I did not need would have shipped in an afternoon, run green forever, and watched nothing that mattered. The hour of recon that killed it is the reason I trust what got built instead.&lt;/p&gt;

&lt;p&gt;This guide is the anatomy of that trust. It is worth nothing that an agent seems careful. What matters is the specific, checkable structure that let this one build a piece of production monitoring I now depend on: a recon step that argues with the task, a finish line a model can actually verify, a plan I approved before any code existed, autonomy bounded by construction rather than by a polite prompt, and acceptance by firing every control instead of watching it sit green.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; anyone pointing a coding agent at real infrastructure, especially self-hosted systems where a quiet failure hides for weeks and "it ran without erroring" is not the same as "it works."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Frefuse-the-task.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Frefuse-the-task.webp" title="The most valuable output of the project was a refusal. More monitoring is not more understanding." alt="Two paths out of a single monitoring request. On the left, blind execution builds a sixth watcher that runs green but redundant while four units keep failing unseen. On the right, a recon probe refutes the premise and a correlation layer turns five existing signals into one incident carrying a likely cause and a runbook. The headline reads: the agent earned trust by refusing the task." width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why should an agent's first job be to refute your task?
&lt;/h2&gt;

&lt;p&gt;In a 2026-07 build, the recon phase refuted the premise of the task before a line of code existed. I had asked for a holistic watcher because "nothing watches the jobs holistically," and a live sweep found five watchers already running. Four background units were failing at that exact moment, invisible because the watcher's coverage list was fixed at install time and never grew as new units shipped.&lt;/p&gt;

&lt;p&gt;This is the crown rule of pointing an agent at infrastructure: recon is not information-gathering, it is error-correction. A first draft of any plan is full of confident, wrong premises, because the person writing the task does not have the live state in front of them. The job of the recon step is to take each premise and try to break it against the real system. "Nothing watches this" breaks the instant you list what is watching it. "Add a host-metrics exporter to one of the nodes" breaks when you find that node already runs a container metrics collector and ships its host metrics through a separate platform, so your exporter would double-count and page on itself.&lt;/p&gt;

&lt;p&gt;The failure mode to design against is the opposite: an agent that takes the task at face value, builds exactly what you said, and hands you a clean diff for the wrong thing. A clean diff describes the code, not the world it runs in. Those four silently-failing units were the proof. Every one of them had been shipped after the monitor's coverage list was written, and the list was a static allowlist that rots the moment the estate grows. The highest-value item on the whole project turned out to be a watcher-of-the-wiring, an audit that asks whether newly-shipped units are actually covered. I never would have thought to ask for it. The recon found it by refusing to believe the task.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Ffive-watchers.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Ffive-watchers.webp" title="Five watchers, fragmented signals. The gap was correlation, not detection, and a coverage list that never grew as the fleet did." alt="Five monitors, a host monitor, container metrics, an init system, and two edge scripts, each emit a fragmented signal into a signal correlator that deduplicates and attributes them into one consolidated incident with a likely cause and a runbook. A side panel shows the existing coverage list still checking two retired units while two newly-shipped units go unchecked, because static coverage drifted as the fleet grew." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes a finish line a model can actually check?
&lt;/h2&gt;

&lt;p&gt;The eleven files this run produced were never seen by the thing that graded it. A small, fast model scored the finish from the transcript alone, never running a command or opening a file. That one constraint rewrites how you write a done condition. "The eleven files exist and the tests pass" is unverifiable to that grader, so the run either loops forever or passes on the agent's word. "One final turn shows fresh command output and a self-audit table" is checkable in a single read.&lt;/p&gt;

&lt;p&gt;An autonomous agent needs a terminal condition, the signal that tells the loop it is finished. The naive version describes the world: files present, tests green, service deployed. The problem is that whatever checks the condition is usually another model, and it has no eyes on your disk. It sees the transcript. A done condition phrased as a fact about the filesystem is therefore either uncheckable, so the run never converges, or taken on faith, so the agent can declare victory while nothing works.&lt;/p&gt;

&lt;p&gt;The fix is to make the finish line transcript-demonstrable. Require the run to surface, in one final turn, the fresh output of real commands: the file listing, the diff, the passing test log, the grep that proves the wiring is live. Then require a self-audit table that maps each acceptance criterion to the evidence for it. Now the grader has something to grade. This is not ceremony. It is the difference between a run that ends because a model felt finished and a run that ends because it showed its work. Write the condition from the grader's real capabilities, not from a template you copied without asking what the grader can see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why gate the plan instead of the finished code?
&lt;/h2&gt;

&lt;p&gt;Before any implementation, the recon collapsed nineteen candidate work-items into a design with exactly four genuinely-open decisions, and I resolved all four in a single structured prompt. Zero of the nineteen items collided with a project already in flight. The approval gate sat on the design, in the middle of the arc, not on the finished pull request at the end, because the middle is the only place an approval is cheap enough to be honest.&lt;/p&gt;

&lt;p&gt;There is a strong pull toward letting an agent run start to finish and reviewing the pull request at the end. For infrastructure, that is the most expensive possible place to learn the plan was wrong. By the time a diff exists, the agent has committed to an architecture, and your choices are approve-with-misgivings or throw away real work. Reviewing a plan costs minutes. Reviewing and then unwinding a wrong build costs a day.&lt;/p&gt;

&lt;p&gt;So the arc has one hard human gate, between research and implementation, and it is the only step that requires me. The agent produces a map: what it will build, what it deliberately will not, every prior decision it reconciled against, and a short list of the forks it genuinely cannot resolve alone. I answer the forks. Everything already settled is asserted plainly so it does not get re-litigated, and only the real decisions reach me. This is also where scope discipline lives. Nineteen items were dispositioned explicitly, each exactly once, so nothing was silently dropped and nothing raced work already running elsewhere. The plan is the artifact you can still change cheaply. Gate there, not at the end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Fgate-cost-curve.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Fgate-cost-curve.webp" title="Review the architecture while disagreement is still cheap. The earlier you gate the plan, the smaller the blast radius of a wrong decision." alt="A rising cost-of-correction curve across five project stages: recon, plan, implement, review, deploy. The cost of fixing a wrong decision climbs from a tenth of a unit at recon to a hundred times at production. A diamond marks plan approval as the cheapest place to disagree, and a crossed-out marker at the finished-code and production end warns against approving there." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you make autonomy safe by construction rather than by prompt?
&lt;/h2&gt;

&lt;p&gt;The remediation layer runs in three tiers, and none of them can take an action, because the capability to act was never written into any of them. The triage model sees only captured text and is never handed a tool it could call. The advice layer emits a proposal into a chat thread and holds no code path that executes. The incident-filing step can open a draft but has no path that closes. Each limit is structural, so a jailbroken or simply confused model hits a wall, not a moment of discretion.&lt;/p&gt;

&lt;p&gt;"Propose-only" written in a system prompt is a request, and a request is not a safety property. The version that holds is the one where the capability is absent. The triage step is the clearest case. Instead of an agent with shell access reasoning about my servers, it is a deterministic collector that captures read-only output and a model that receives that text and returns structured JSON. The model cannot run anything, because nothing runnable was ever put in its hands. Its blast radius is the size of a JSON object.&lt;/p&gt;

&lt;p&gt;The same shape repeats up the stack. The layer that suggests a fix writes the suggestion to a thread for a human to read; no branch anywhere in it executes the fix, so no prompt can talk it into one. The step that records an incident can draft a writeup, and the rule that an incident closes only after a human has confirmed the root cause and verified the fix is enforced by that step having no close path at all. This is graduated autonomy done honestly: each tier holds exactly the capability it has earned, and the tiers above it are not one clever sentence away. When you catch yourself trusting a model because you told it to behave, you are holding a prompt, not a gate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Fblast-radius.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Fblast-radius.webp" title="Read-only in, a structured proposal out, human approval before any change. A safe agent is one that has nowhere else to go." alt="A three-tier capability boundary between live infrastructure and an agent. A deterministic collector takes read-only telemetry and runs approved commands with no mutation path, then hands captured text to a triage model that returns structured output only, so its maximum blast radius is one JSON object. The model emits a proposal into a human-reviewed thread with no auto-execution branch. Side panels list what the design stops: direct model action, silent infrastructure changes, prompt-only safety theater, and hidden execution paths." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why isn't "armed and green" the same as working?
&lt;/h2&gt;

&lt;p&gt;Every control that shipped was accepted by firing it, never by watching it sit healthy. We injected a synthetic fault and watched it travel the whole path to a chat thread. We ran one real triage cycle end to end and read the verdict it produced. We handed the refusal paths a case they should reject and confirmed they rejected it. A monitor that is armed and green has told you nothing at all until you have made it catch something.&lt;/p&gt;

&lt;p&gt;The most seductive lie in operations is the green dashboard. An alerting rule with no alert, a remediation daemon with a healthy badge, a heartbeat that has never missed: all of them look identical whether they work or are quietly dead. "Armed and green" is a claim about a control's own status, not about whether it does its job. I have been burned by a reaper that reported healthy for weeks while reaping nothing, because nothing had ever forced it to act and reveal that it could not.&lt;/p&gt;

&lt;p&gt;So the acceptance bar for the controls here was a fired proof. For the coverage audit, ship a unit it should flag and confirm the flag. For the triage path, run a real cycle and read the result. For the propose-only guarantee, feed it a case that must be refused and confirm the refusal returns an error instead of an action. The heartbeat is the honest exception worth naming: rather than trust a new bespoke alert, wire it onto the estate's already-proven dead-man monitor, the same one that pages when any other producer goes silent, so it inherits a firing that was tested long ago. A control you have watched catch a real event is a fact. A control you have only watched stay green is a hope wearing the costume of a fact. The extra hour spent writing synthetic faults is the cheapest insurance you will buy on infrastructure, because the alternative is discovering the control was decorative during the incident it existed to catch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Fgreen-is-not-proof.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Fgreen-is-not-proof.webp" title="A control that has never fired is still an untested claim. Observe the status, trigger the control, then verify the outcome." alt="Two monitoring panels side by side. On the left, armed and green: a coverage audit, a triage pipeline, and a refusal control all report healthy, but the events-caught and controls-fired counters are empty. On the right, fired and proven: a synthetic fault is injected and the controls actually catch it, with missing coverage detected, a triage verdict produced, an unsafe action refused, and the incident reaching a chat thread." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What do the five checkpoints add up to?
&lt;/h2&gt;

&lt;p&gt;Across the project, five practices did the load-bearing work, and each one guards against a different way agent-built infrastructure fails silently. Not one of them depends on the model being smart. Every one of them is structure the human keeps: an argument before the build, a finish line a grader can read, a gate on the plan, capability bounded by construction, and acceptance by firing the control.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Ftrust-harness.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fagent-infra-trust%2Ftrust-harness.webp" title="Five checkpoints between a task and production. Trusted infrastructure is not built by autonomy alone, it is built by checkpoints; remove one and that class of silent failure ships." alt="The five-checkpoint trust harness, drawn as a pipeline from a task to trusted infrastructure. Each checkpoint stops a distinct failure: refute stops the wrong task, demonstrate stops the unverifiable finish, gate stops the wrong architecture, constrain stops the unauthorized action, and fire stops the decorative control. A continuous-learning loop feeds evidence and outcomes back to sharpen every checkpoint." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checkpoint&lt;/th&gt;
&lt;th&gt;The silent failure it prevents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recon that refutes the task&lt;/td&gt;
&lt;td&gt;building the wrong thing, cleanly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transcript-demonstrable finish line&lt;/td&gt;
&lt;td&gt;a run that ends on the model's own word&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval gate on the plan, not the PR&lt;/td&gt;
&lt;td&gt;learning the architecture was wrong after the work is done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autonomy bounded by construction&lt;/td&gt;
&lt;td&gt;a prompt talked into taking an action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acceptance by firing the control&lt;/td&gt;
&lt;td&gt;a decorative monitor that is green and dead&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The through-line is that trust in agent-built infrastructure is not a feeling you develop about a capable model. It is a property of the harness around it. The model in this arc was the same model that wrote the confident, wrong first draft of the plan. What made its output trustworthy was that every step had a place where a wrong assumption got caught by something other than the model's own judgment. Recon caught the wrong premise. The grader caught the unfinished run. I caught the wrong plan. Construction caught the unsafe action. A fired test caught the decorative control.&lt;/p&gt;

&lt;p&gt;Point a fast writer at your infrastructure with none of that, and you get exactly what the speed implies: the wrong thing, built well, running green, watching nothing. Keep the five checkpoints, and the agent becomes what it should be, a very fast pair of hands on a short and very well-lit leash. That is not a limit on what agents can do for operations. It is the thing that makes handing them the keys a sane decision instead of a hopeful one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.danmercede.com/guides/agent-built-infrastructure-you-can-trust" rel="noopener noreferrer"&gt;danmercede.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Rekindle: your abandoned side project misses you 🔥</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:10:09 +0000</pubDate>
      <link>https://dev.to/danmercede/rekindle-your-abandoned-side-project-misses-you-23c0</link>
      <guid>https://dev.to/danmercede/rekindle-your-abandoned-side-project-misses-you-23c0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Every developer has one: the side project you started with real love, pushed afew passionate commits to, and then... life. The tab closed. The flame wentquiet. Mine is &lt;a href="https://github.com/OrionArchitekton/spark" rel="noopener noreferrer"&gt;spark&lt;/a&gt;, an intentionsetting app I started for my partner and me as the new year turned. One commit.190 days of silence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rekindle&lt;/strong&gt; is a coach for exactly that repo. Paste a dormant GitHub projectand it gives you three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A diagnosis&lt;/strong&gt;: why the flame died, read honestly from your commit cadenceand the gap between README ambition and what got built.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rekindle plan&lt;/strong&gt;: three steps, the first one deliberately small enough todo in under 15 minutes tonight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A corner speech&lt;/strong&gt;: a roughly 90-word hype speech about YOUR project, byname, delivered out loud like a cornerman between rounds.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u9bza556ygfcbp9un4d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8u9bza556ygfcbp9un4d.png" alt=" " width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The theme is passion. This is the tool for the week after the passion: the onethat gets you to open the project again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Live app: &lt;strong&gt;&lt;a href="https://rekindle-sooty.vercel.app" rel="noopener noreferrer"&gt;https://rekindle-sooty.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Try it on any public GitHub repo you've gone quiet on. If you just want to seea result instantly, &lt;a href="https://rekindle-sooty.vercel.app/?demo=1" rel="noopener noreferrer"&gt;this link&lt;/a&gt;renders a frozen real result (a live run against my abandoned &lt;code&gt;spark&lt;/code&gt; repo,with the real ElevenLabs audio). Everything else is computed live per request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41vvgzarj0vca5vh56w8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F41vvgzarj0vca5vh56w8.png" alt=" " width="800" height="1062"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/OrionArchitekton" rel="noopener noreferrer"&gt;
        OrionArchitekton
      &lt;/a&gt; / &lt;a href="https://github.com/OrionArchitekton/rekindle" rel="noopener noreferrer"&gt;
        rekindle
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Your abandoned side project misses you. Gemini diagnoses why the flame died; ElevenLabs delivers the comeback speech. DEV Weekend Challenge: Passion Edition.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🔥 Rekindle&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Your abandoned side project misses you.&lt;/p&gt;
&lt;p&gt;Paste a dormant GitHub repo. Rekindle reads its README, recent commits, and
top-level files, then delivers three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A diagnosis&lt;/strong&gt;: why the flame died, read honestly from the commit cadence and
the gap between README ambition and reality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rekindle plan&lt;/strong&gt;: three steps, the first one small enough to do in under 15
minutes tonight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A corner speech&lt;/strong&gt;: a roughly 90-word hype speech about YOUR project, by
name, spoken out loud like a cornerman between rounds.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/weekend-2026-07-09" rel="nofollow"&gt;DEV Weekend Challenge: Passion Edition&lt;/a&gt;
because passion is also the love that fuels late-night side projects, and every
developer has one whose flame went quiet.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;GitHub URL --&amp;gt; GitHub API (README, commits, tree)
                 |
                 v
          Gemini 2.5 Flash (temperature 0, JSON output)
                 |  diagnosis + embers + plan + hype speech
                 v
          ElevenLabs TTS --&amp;gt; your&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/OrionArchitekton/rekindle" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Next.js 16 (App Router) with two server routes and a deliberately smallsurface:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub URL --&amp;gt; GitHub API (README, last commits, file tree)
                 |
                 v
        Gemini 2.5 Flash (temperature 0, JSON output, thinking off)
                 |   diagnosis + embers + plan + hype speech
                 v
        ElevenLabs TTS --&amp;gt; the corner speech, out loud
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Google AI does the reading.&lt;/strong&gt; &lt;code&gt;/api/rekindle&lt;/code&gt; snapshots the repo (metadata,README excerpt, last 10 commits, top-level files) and hands it to Gemini 2.5Flash with &lt;code&gt;temperature: 0&lt;/code&gt;, &lt;code&gt;responseMimeType: application/json&lt;/code&gt;, and thinkingdisabled, so output is fast, stable, and cheap. The interesting part wasthe failure I hit first: with thinking enabled, Gemini's reasoning tokens countagainst &lt;code&gt;maxOutputTokens&lt;/code&gt;, so my 2048 cap silently truncated the JSON mid-fieldand my parser (correctly) rejected it. Thinking off + a bigger cap fixed it.The model output is schema-validated and length-clamped server-side; anythingmalformed renders as an error, never as half-parsed UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ElevenLabs does the believing.&lt;/strong&gt; The hype speech only lands when you hear it.One catch: a public TTS endpoint on a server-side key is a free-for-all walletdrain, so &lt;code&gt;/api/rekindle&lt;/code&gt; HMAC-signs the speech text it generates and&lt;code&gt;/api/speak&lt;/code&gt; refuses anything unsigned (I verified the refusal with aforged-token test, and rate limits sit on both routes). Nobody gets to use mykey as a generic text-to-speech proxy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest demo mode.&lt;/strong&gt; &lt;code&gt;/?demo=1&lt;/code&gt; server-renders a frozen result so the demo isdeterministic. The frozen content is a real captured output from a live run,checked in with its matching real audio, and the page discloses it.&lt;/p&gt;

&lt;p&gt;Unit tests cover the pure core (URL parsing, prompt build, defensive JSONparse). Built end-to-end in a Claude Code session (Claude Fable 5); the committrailers carry the co-author record, and I made the calls on idea, scope, andsubmission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Google AI&lt;/strong&gt; and &lt;strong&gt;Best Use of ElevenLabs&lt;/strong&gt;: Gemini 2.5 Flashreads the repo and writes the diagnosis, plan, and speech as strict JSON;ElevenLabs (&lt;code&gt;eleven_multilingual_v2&lt;/code&gt;) turns the speech into the voice in yourcorner. The two are chained: what Gemini writes about your specific repo isexactly what ElevenLabs says out loud.&lt;/p&gt;

&lt;p&gt;Thanks for reading. Now go run &lt;code&gt;npm install&lt;/code&gt; on the one you know I'm talkingabout. 🔥&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>ai</category>
      <category>googleai</category>
    </item>
    <item>
      <title>Building Engram in 48 hours: what a memory engine taught me about forgetting</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:01:42 +0000</pubDate>
      <link>https://dev.to/danmercede/building-engram-in-48-hours-what-a-memory-engine-taught-me-about-forgetting-5d6</link>
      <guid>https://dev.to/danmercede/building-engram-in-48-hours-what-a-memory-engine-taught-me-about-forgetting-5d6</guid>
      <description>&lt;p&gt;I entered the Qwen Cloud Global AI Hackathon's MemoryAgent track with a bias: I havespent months maintaining a typed memory system for my own AI agents (memory fileswith decay, an index under a hard size budget, contradiction notes). The track briefread like a checklist of the things that system taught me the hard way: efficientretrieval, timely forgetting, recall within limited context windows. So I productizedthe doctrine into an engine: Engram.&lt;/p&gt;

&lt;p&gt;Three things earned their keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The budget is the feature.&lt;/strong&gt; Most memory demos retrieve top-k and pray it fits.Engram scores every active memory (embedding similarity from text-embedding-v4,blended with retention, importance, and access frequency) and greedy-packs under ahard token budget, with backfill so a huge top memory does not starve the context.The packer has a property test over randomized pools: the selected set never exceedsthe budget. Writing that test first changed the design; the naive version failed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Forgetting needs a mechanism, not a cron job.&lt;/strong&gt; Retention decays exponentiallyfrom last access with type-specific half-lives (episodes die in weeks, skills livefor months), scaled by importance. Recall refreshes retention, so what gets usedstays alive. Below the floor, a memory is marked decayed but never deleted; theaudit trail is the product too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Contradictions are the hard 20 percent.&lt;/strong&gt; New facts are checked against nearestneighbors and adjudicated by qwen3.7-plus at temperature 0 with strict JSON and afail-safe parser (a parse failure can never supersede anything). The subtle bug anadversarial review caught: my similarity gate was tuned too high, so "I moved toDenver" never even reached the adjudicator against "I live in San Diego" (they onlycosine at about 0.49 in 256 dims). Live probes, not intuition, set the threshold.&lt;/p&gt;

&lt;p&gt;The meta-lesson: I ran an adversarial review pass that tried to refute 75 discreteclaims in my own README and submission against the code and live probes. It found aswapped argument pair feeding the adjudication prompt, the mistuned gate above, andhalf a dozen places where my wording was stronger than my evidence. An hour of beingrefuted is worth more than a day of polishing.&lt;/p&gt;

&lt;p&gt;Engram is MIT-licensed, ships an MCP server so any agent can adopt it as a memorybackend, and runs on Alibaba Cloud with Qwen Cloud models end to end.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/OrionArchitekton/engram" rel="noopener noreferrer"&gt;https://github.com/OrionArchitekton/engram&lt;/a&gt;&lt;br&gt;
Live: &lt;a href="https://engram.orionbot.online" rel="noopener noreferrer"&gt;https://engram.orionbot.online&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>alibabachallenge</category>
      <category>agents</category>
      <category>community</category>
    </item>
    <item>
      <title>The Router Is a Trap: Running Other Models Under Claude Code, Off Their Own Subscriptions</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Wed, 08 Jul 2026 22:17:23 +0000</pubDate>
      <link>https://dev.to/danmercede/the-router-is-a-trap-running-other-models-under-claude-code-off-their-own-subscriptions-13n0</link>
      <guid>https://dev.to/danmercede/the-router-is-a-trap-running-other-models-under-claude-code-off-their-own-subscriptions-13n0</guid>
      <description>&lt;p&gt;I wanted a simple thing. Keep a Claude model orchestrating inside Claude Code, but when it fans out to subagents and background jobs, run that work on other models paid for by other subscriptions, so it stops drawing down my Anthropic allocation. The obvious build is a router: point Claude Code at a proxy, let the proxy translate each call to Gemini or GPT or a local model. That is the popular path, and it is a trap.&lt;/p&gt;

&lt;p&gt;Two findings flipped my plan. The router path corrupts tool calls badly enough to be useless for agentic subagents. And the constraint that actually decides the architecture is legal, not technical: reusing a consumer subscription inside a third-party harness is prohibited and actively enforced at three of the four vendors I cared about. What survives both problems is the unglamorous option I already had running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; anyone trying to cut their Claude Code bill by offloading subagent or workflow fan-out onto other models, and anyone about to install a community "just point it at this proxy" bridge without reading what it does to your sandbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  What are you actually trying to buy?
&lt;/h2&gt;

&lt;p&gt;The goal is narrow and worth stating before the tooling: keep the frontier orchestrator (a Claude model) as Claude Code's primary driver, and route only the &lt;em&gt;fan-out&lt;/em&gt; (subagents, workflow &lt;code&gt;agent()&lt;/code&gt; calls, background tasks) to other models on their own billing. A Claude Code Max plan meters a weekly token allocation that resets on a fixed cadence, so every subagent you can move off that pool is allocation you keep for the orchestration that has to stay on Claude.&lt;/p&gt;

&lt;p&gt;The key word is &lt;em&gt;off-budget&lt;/em&gt;. This is the test every option below has to pass. Running a cheaper Claude model as a subagent does not pass it: Claude Code's native per-subagent selection is Anthropic-alias-only, and a Haiku or Sonnet subagent still bills the same Anthropic pool. Saving per-token cost is not the same as taking work off the allocation. Only routing to a genuinely external provider does that, which is why the interesting question is how to reach a non-Anthropic model at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does the router path corrupt your tool calls?
&lt;/h2&gt;

&lt;p&gt;The most-starred tool in this space, claude-code-router, sits at roughly 35,600 GitHub stars as of mid-2026, and it works by exploiting one documented fact: Claude Code does not validate model IDs behind a custom &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt;. Anthropic's own model-config docs say the ID check "runs only on the Anthropic API," so behind a gateway your provider defines the names and Claude Code passes any string through unchecked. That single gap is what every under-the-harness router is built on.&lt;/p&gt;

&lt;p&gt;It works at the transport layer and fails at the semantic one. The dominant, recurring failure is tool-calling corruption. When a non-Claude model runs behind Claude Code's Anthropic-shaped API, the translation shim mistranslates tool calls: optional parameters get injected as empty strings (a &lt;code&gt;Read&lt;/code&gt; arrives with &lt;code&gt;pages: ""&lt;/code&gt;), which Claude Code rejects, "producing an infinite loop that burns reasoning tokens until the user manually interrupts." Server tools like web search get mistranslated into ordinary functions, so search comes back empty or misleading. Some local-model adapters emit plain text that merely &lt;em&gt;mimics&lt;/em&gt; a tool call and then does nothing with it. On top of that, documented proxy layers drop MCP server tools, ignore prompt-cache control, and turn parallel tool use into sequential calls.&lt;/p&gt;

&lt;p&gt;For a chat turn, that degradation is survivable. For an agentic subagent whose entire job is to call &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Edit&lt;/code&gt;, &lt;code&gt;Bash&lt;/code&gt;, and MCP tools reliably, it is disqualifying. The proxy gives you the model behind the endpoint and takes away the tool loop that made the endpoint worth reaching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is the Terms of Service the real constraint, not the tooling?
&lt;/h2&gt;

&lt;p&gt;Here is the finding that actually decided the architecture: as of mid-2026, reusing a consumer &lt;em&gt;subscription&lt;/em&gt; inside a third-party harness is prohibited and enforced at three of the four vendors, so the tooling question is downstream of a legal one. The enforcement is not theoretical, and in one case it is not even loud.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic&lt;/strong&gt; made the prohibition explicit in a February 2026 consumer-terms revision (tokens permitted only in Claude Code and Claude.ai), and on April 4, 2026 blocked third-party harnesses from using Claude Max subscription rate limits outright. Bring-your-own API keys still work; subscription-OAuth reuse does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt; is the cautionary tale. It deployed abuse detection and &lt;em&gt;silently&lt;/em&gt; banned paying AI Ultra subscribers (a $249.99-per-month plan) who drove third-party CLIs on their consumer OAuth in mid-April 2026, with reports of the ban cascading to Gmail and Workspace. It then stopped serving consumer and Pro or Ultra tiers to its IDE code-assist extensions on June 18, 2026. If your estate leans on Google Workspace, an OAuth-reuse ban is not a coding-tool outage, it is a Gmail outage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;xAI's&lt;/strong&gt; terms bar automated, non-human (bot or script) access to Grok and reserve suspension at sole discretion; the only sanctioned programmatic path is the separate pay-per-token API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI is the outlier.&lt;/strong&gt; It publicly sanctions reusing a ChatGPT subscription in third-party tools with "the same usage as Codex," and Codex-into-third-party has an official adapter. Subscription Codex is governed by the consumer ChatGPT terms and metered as plan usage, not raw API billing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The net for the original dream of "route all my subscriptions through one proxy": it is durable for exactly one of them, GPT via Codex. Every other subscription seam rides reverse-engineered OAuth that the vendor is actively closing. One corroborating datum: a Claude-Max-as-API seam that made the rounds earlier in 2026 survived about two months before the vendor shut it. Treat any subscription-brokering feature as a liability with a short shelf life, not a capability. For Gemini or Grok specifically, use a paid API key, never the consumer login.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works: shell out to headless CLIs
&lt;/h2&gt;

&lt;p&gt;The mechanism that clears both the tool-fidelity trap and the Terms-of-Service wall is the one I already ran in production for months: keep Claude orchestrating and shell out to each other vendor's &lt;em&gt;headless CLI&lt;/em&gt; as a subagent. Instead of translating Claude Code's API to another provider, you invoke &lt;code&gt;codex exec&lt;/code&gt;, &lt;code&gt;gemini&lt;/code&gt;, or &lt;code&gt;grok -p&lt;/code&gt; as a subprocess, each authenticated to its own login, each doing its own native tool calling.&lt;/p&gt;

&lt;p&gt;This inverts every problem the proxy created. There is no Anthropic-to-OpenAI translation shim, so there is no empty-string-parameter loop and no mistranslated web search; each CLI speaks its own protocol natively. The work runs on that CLI's own credentials, so a Codex call bills the ChatGPT plan and a local-model call bills nothing, both off the Anthropic allocation. And it matches a doctrine most mature harnesses already follow, which is to prefer a working CLI over a bespoke integration: a post-push review pipeline that drives Codex headless is the same pattern, just pointed at review instead of fan-out.&lt;/p&gt;

&lt;p&gt;The tradeoff is honest. You give up the illusion of one unified &lt;code&gt;/v1&lt;/code&gt; endpoint and instead orchestrate N processes, joined by your own wait-and-collect logic. In exchange you get native tool loops, per-vendor billing, and no dependency on a subscription seam that gets patched out from under you.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you keep the delegation layer from bypassing your sandbox?
&lt;/h2&gt;

&lt;p&gt;The security catch is real and specific: nearly every community CLI-bridge I looked at launches its child CLI with a permission-or-sandbox bypass flag baked in (&lt;code&gt;--dangerously-skip-permissions&lt;/code&gt;, &lt;code&gt;--dangerously-bypass-approvals-and-sandbox&lt;/code&gt;, &lt;code&gt;--yolo&lt;/code&gt;, &lt;code&gt;-y&lt;/code&gt;). That is convenient and it is also how you hand an untrusted subagent's output the keys to your workstation. When I shipped my own delegation layer in July 2026, the rails were the point, not an afterthought, and they are enforced in code with tests, not just documented.&lt;/p&gt;

&lt;p&gt;Five rails carry the safety, and each is one small function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never emit a bypass flag.&lt;/strong&gt; A denylist is asserted in a unit test so a bypass flag can never regress into the spawned &lt;code&gt;argv&lt;/code&gt;. The read-only sandbox is the default; write access is an explicit, separate opt-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrub the child environment by whitelist, not denylist.&lt;/strong&gt; The child gets &lt;code&gt;PATH&lt;/code&gt;, &lt;code&gt;HOME&lt;/code&gt;, and its &lt;em&gt;own&lt;/em&gt; provider key, and nothing else. A future secret-shaped variable you have not thought of yet does not survive by default, because the default is to drop, not to keep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve the CLI by absolute path.&lt;/strong&gt; Never exec a bare command name; resolve it to an absolute path first so a planted binary earlier on &lt;code&gt;PATH&lt;/code&gt; cannot shadow the real one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close stdin.&lt;/strong&gt; Recent Codex versions deadlock on a non-TTY pipe, so the child gets &lt;code&gt;stdin&lt;/code&gt; pointed at &lt;code&gt;/dev/null&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# read-only by default; workspace-write is an explicit, separate opt-in
&lt;/span&gt;&lt;span class="n"&gt;sandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace-write&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read-only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;argv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;resolve_cli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;argv&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;BYPASS_FLAGS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# tested, not just asserted here
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fifth rail I only found by running the thing, which is the reminder that live verification beats reasoning. Grok's CLI needs an explicit headless flag: &lt;code&gt;grok -p "&amp;lt;prompt&amp;gt;"&lt;/code&gt; prints the answer and exits, but a &lt;em&gt;positional&lt;/em&gt; prompt with no flag opens the interactive TUI and hangs a non-interactive caller. A positional prompt cost me a two-minute timeout before I read the help text. The regression is now pinned by a test that asserts the &lt;code&gt;-p&lt;/code&gt; flag is present in the built &lt;code&gt;argv&lt;/code&gt;.&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="c1"&gt;# a positional prompt opens grok's interactive TUI and hangs a non-TTY caller;
# -p / --single prints and exits.
&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;resolve_cli&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--output-format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What should you actually build?
&lt;/h2&gt;

&lt;p&gt;If you take one architecture from this, take this shape, which I settled on after two research runs and a working implementation in July 2026: Claude orchestrates, and it delegates to a small, gated set of headless CLIs chosen by Terms-of-Service durability first and cost second.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Orchestrator:&lt;/strong&gt; a Claude model in Claude Code, unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Primary off-budget subagent:&lt;/strong&gt; GPT via &lt;code&gt;codex exec&lt;/code&gt; on the ChatGPT plan. This is the one durable subscription-reuse path, so it earns the default slot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap bulk fan-out:&lt;/strong&gt; a metered API key (DeepSeek and Kimi run roughly an order of magnitude cheaper than frontier Claude on input tokens) or a free local model on your own GPU box via a local inference endpoint. No subscription to reuse means no ban risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini or Grok, if at all:&lt;/strong&gt; only on a paid API key, isolated from the primary Google account. Never the consumer login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mechanism:&lt;/strong&gt; headless-CLI subprocess, not a proxy. If you ever want a proxy for &lt;em&gt;API-key&lt;/em&gt; models, the routers are fine; just treat their subscription-brokering as the liability it is.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The three paths compared, so the choice is legible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Router proxy&lt;/th&gt;
&lt;th&gt;Headless CLI (recommended)&lt;/th&gt;
&lt;th&gt;Native per-subagent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keeps Claude orchestrating&lt;/td&gt;
&lt;td&gt;Only if you patch aliases&lt;/td&gt;
&lt;td&gt;Yes, cleanly&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routes subagents to other providers&lt;/td&gt;
&lt;td&gt;Yes, but Claude-unaware&lt;/td&gt;
&lt;td&gt;Yes, per call, model named&lt;/td&gt;
&lt;td&gt;No, Anthropic aliases only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Off the Anthropic budget&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No, still bills the same pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool-calling reliability&lt;/td&gt;
&lt;td&gt;Poor and brittle&lt;/td&gt;
&lt;td&gt;Native per CLI&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscription legality&lt;/td&gt;
&lt;td&gt;Rides the prohibited seam&lt;/td&gt;
&lt;td&gt;Depends on vendor (GPT sanctioned)&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The plan I walked in with, a single elegant router, was the wrong instinct dressed up as the clean one. The winning design is a handful of subprocesses behind a security gate, choosing each model by whether its vendor will still let you in next month. Boring, durable, and off-budget. That is the trade, and it is a good one.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The vendor terms and enforcement dates in this guide are time-sensitive and were accurate to mid-2026; enforcement is active and moving, so re-verify a vendor's current terms before you build on any subscription seam.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.danmercede.com/guides/off-budget-subagents-under-claude-code" rel="noopener noreferrer"&gt;danmercede.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>claudecode</category>
      <category>llmops</category>
    </item>
    <item>
      <title>Giving Your Coding Agent Web Access Without Wrecking Its Context Window</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Mon, 06 Jul 2026 03:30:59 +0000</pubDate>
      <link>https://dev.to/danmercede/giving-your-coding-agent-web-access-without-wrecking-its-context-window-gi9</link>
      <guid>https://dev.to/danmercede/giving-your-coding-agent-web-access-without-wrecking-its-context-window-gi9</guid>
      <description>&lt;p&gt;Most coding agents get web access the lazy way: a fetch tool that returns raw HTML, a search tool wired straight into the main context, and a metered API behind both. It works in a demo. Then the agent reads three documentation pages, and half its context window is &lt;code&gt;&amp;lt;div class="nav"&amp;gt;&lt;/code&gt; and inline analytics.&lt;/p&gt;

&lt;p&gt;I spent a day hardening the web-access layer for my own agents and measured every option on two axes almost nobody ranks first: &lt;strong&gt;cost&lt;/strong&gt; and &lt;strong&gt;rate-limit resilience&lt;/strong&gt;. The result is a stack that is free or self-hosted for the load-bearing paths, plus one structural pattern that matters more than any single tool: keep raw pages out of the main context by design.&lt;/p&gt;

&lt;p&gt;This guide is the version I wish I had at the start. Every number is from a public source or a run you can repeat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; anyone giving an autonomous agent (Claude Code, a &lt;code&gt;-p&lt;/code&gt; headless pipeline, Codex, an in-house harness) the ability to search, read, and drive the web, and paying for the tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does giving an agent web access wreck its context window?
&lt;/h2&gt;

&lt;p&gt;Because raw HTML is mostly not content. In one measured fetch, the same page was &lt;strong&gt;9,541 tokens as raw HTML and 1,678 tokens as clean markdown, an 82% reduction&lt;/strong&gt;. Typical HTML-to-markdown conversion lands nearer 20 to 30%, but the direction is constant: you are paying to put boilerplate into the model's attention, then paying again on every following turn.&lt;/p&gt;

&lt;p&gt;The waste compounds three ways. &lt;strong&gt;Context flood:&lt;/strong&gt; a large docs page is roughly 25,000 tokens of input, and it stays resident for the rest of the conversation. &lt;strong&gt;Cost:&lt;/strong&gt; search results and fetched content are billed as input tokens on the turn they arrive and every turn after, so a page you read once is re-billed until it ages out. &lt;strong&gt;Rate limits:&lt;/strong&gt; point a fan-out of parallel agents at a metered search API and you hit &lt;code&gt;429&lt;/code&gt; almost immediately. The lazy path fails on all three at once.&lt;/p&gt;

&lt;p&gt;Context rot is the quiet one. Even on a large context window, recall degrades as the window fills. A page of HTML you never needed is not free just because it fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the three axes of agent web access?
&lt;/h2&gt;

&lt;p&gt;Web access is not one capability, it is three, and they want different tools. &lt;strong&gt;Search&lt;/strong&gt; (find the URLs), &lt;strong&gt;fetch&lt;/strong&gt; (read a page without drowning in it), and &lt;strong&gt;browser automation&lt;/strong&gt; (click, fill, scroll a live app). In a 2026-07 review of the field, the cheapest resilient answer differed per axis: self-hosted for search and fetch, local-and-free for the browser.&lt;/p&gt;

&lt;p&gt;Conflating them is how people end up overpaying. You do not need a headless Chromium to read a blog post; an extract-first markdown fetch handles the large majority of retrieval. You do not need a metered search API to read a page whose URL you already have. Name the axis, then pick the cheapest tool that covers it. Reserve the expensive, stateful browser for genuine interaction: clicks, forms, infinite scroll, auth walls.&lt;/p&gt;

&lt;p&gt;The rest of this guide takes the axes in order, cheapest and most rate-limit-resilient first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the cheapest, most rate-limit-resilient search stack?
&lt;/h2&gt;

&lt;p&gt;Self-hosting wins outright. A self-hosted &lt;strong&gt;SearXNG&lt;/strong&gt; metasearch instance runs on a 5 to 10 dollar per month VPS, aggregates roughly 95 to 200 engines (Google, Bing, Brave, DuckDuckGo and more) behind a JSON API, and charges nothing per query. Put a Valkey or Redis cache in front of it, because individual upstream engines rate-limit a single client IP hard, and the cache is what absorbs that.&lt;/p&gt;

&lt;p&gt;If you want a hosted API instead, rank by the free tier and the rate cap, not the marketing. &lt;strong&gt;Exa&lt;/strong&gt; gives 20,000 free requests a month. &lt;strong&gt;Brave&lt;/strong&gt; gives about 1,000 free queries a month but caps the free tier at &lt;strong&gt;1 query per second&lt;/strong&gt;, which breaks the instant you do parallel fan-out search, a common agent pattern. &lt;strong&gt;Perplexity Sonar&lt;/strong&gt; and &lt;strong&gt;Kagi&lt;/strong&gt; are metered with no meaningful free tier. Anthropic's native &lt;code&gt;web_search&lt;/code&gt; is the priciest per call at &lt;strong&gt;10 dollars per 1,000 searches&lt;/strong&gt; plus token costs, though its companion &lt;code&gt;web_fetch&lt;/code&gt; adds no per-fetch fee and ships a &lt;code&gt;max_content_tokens&lt;/code&gt; cap worth using.&lt;/p&gt;

&lt;p&gt;One honest caveat carried from the research: a "Serper plus a reader" stack was reported at about 11 dollars a month for 10,000 searches and 10,000 extractions, against roughly 96 dollars for a single-vendor API on the same workload. That specific figure rests on a single blog and should be read as directional. The ranking (self-hosted free, then Exa/Brave free tiers, then metered) is the corroborated part.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the cheapest way to read a page without flooding context?
&lt;/h2&gt;

&lt;p&gt;Extract-first, markdown, in that order. The single biggest lever is converting a page to clean markdown before it reaches the model, which cut tokens 82% in the measured case above. &lt;strong&gt;Jina Reader&lt;/strong&gt; is the cheap default: prefix any URL with the reader endpoint and you get markdown back, free at 20 requests per minute with no key, or 10 million tokens on a free key, and it is open-source so you can self-host it. &lt;strong&gt;Trafilatura&lt;/strong&gt; is a self-hosted Python extractor with no API cost and strong extraction quality, though it will not render JavaScript.&lt;/p&gt;

&lt;p&gt;For scraping proper, the same free-first ranking holds. &lt;strong&gt;Crawl4AI&lt;/strong&gt; is Apache-2.0 with no per-page fees; your only cost is the box it runs on. Managed &lt;strong&gt;Firecrawl&lt;/strong&gt; trades that for convenience at roughly 83 dollars a month on its standard tier. The crossover is volume: at low and mid volume the managed credits are cheaper than running infrastructure; past high volume, the self-hosted crawler wins. Pick the tool, but do the markdown conversion no matter which you pick. Handing a model raw HTML is the mistake; the extractor is incidental.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a thin CLI beat an MCP server for agent web tools?
&lt;/h2&gt;

&lt;p&gt;Because an MCP server costs context on every host, and a CLI costs almost none. As of 2026-07, Claude Code defers MCP tool schemas until a tool is invoked, so an MCP is cheapish there. But Codex has no such deferral: every MCP schema loads in full, every session, and a harness that already carries a dozen servers pays that tax constantly. A thin CLI you shell out to via Bash costs zero resident context until the moment you call it.&lt;/p&gt;

&lt;p&gt;So for anything I want available on more than one agent host, I ship a CLI, not an MCP. I built three this way: a search CLI over a grounded-answer API, a live-social search CLI, and a page-reader over Jina. Each self-wraps its own secret retrieval (read the key from the environment, or transparently re-run under a secrets manager if it is not set) so the caller passes no credentials, and each takes a &lt;code&gt;--json&lt;/code&gt; flag so an agent can parse it. One implementation, every host, near-zero context. The rule generalizes: prefer a working CLI over its MCP unless the tool genuinely needs a stateful session.&lt;/p&gt;

&lt;p&gt;Browser automation is the one real exception. Playwright, driving a real browser, is stateful and does not reduce cleanly to a stateless CLI, so it stays an MCP or an in-process library. There, the free-and-local option still wins: Playwright is Apache-2.0, runs on your own machine at zero LLM cost, and hits about 98% reliability on deterministic scripts. Running Playwright's MCP in its CLI mode reportedly cuts token use about 4x versus the protocol mode. Metered cloud browsers exist (Browserbase, Stagehand's hosted tier) but the free tiers are thin (Browserbase caps the free tier at 1 browser-hour, 3 concurrent sessions, 15-minute sessions), so reserve them for scale you have actually hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you build the fetch tool, and why run it in a subagent?
&lt;/h2&gt;

&lt;p&gt;Because the tool only wins if the raw page never reaches the main agent at all. The pattern is small: a fetch CLI that returns bounded markdown, invoked inside a subagent whose job is to read the page and return only the distilled slice the main agent asked for. The main context sees a paragraph, not 25,000 tokens. Even the markdown never lands in the parent conversation.&lt;/p&gt;

&lt;p&gt;The fetch CLI itself is about 120 lines. The load-bearing parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;  &lt;span class="c1"&gt;# the full CLI also imports json (for --json) and sys
&lt;/span&gt;
&lt;span class="n"&gt;READER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://r.jina.ai/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;# The default python-urllib User-Agent gets a 403 from the reader's bot filter.
# Send a real browser UA. This one line is the difference between 200 and 403.
&lt;/span&gt;&lt;span class="n"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WEB_READ_UA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(KHTML, like Gecko) Chrome/126 Safari/537.36&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;READER&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Return-Format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                  &lt;span class="c1"&gt;# optional, raises free-tier limits
&lt;/span&gt;        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&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;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# A stdlib token estimate: ~4 chars per token. Label it estimated.
&lt;/span&gt;    &lt;span class="n"&gt;est&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;est&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;est&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;... [truncated]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details earn their place. It is &lt;strong&gt;keyless by default&lt;/strong&gt; (the reader's free tier needs no key), so the tool works with zero setup and only reaches for a key to raise limits. It carries a &lt;strong&gt;token budget&lt;/strong&gt; (&lt;code&gt;--max-tokens&lt;/code&gt;), so a runaway page cannot blow the context even inside the subagent. And it emits &lt;strong&gt;&lt;code&gt;--json&lt;/code&gt;&lt;/strong&gt; so the calling agent gets structured output, not a wall of text. The browser User-Agent is the one non-obvious line: a live smoke test, not the unit tests, is what caught the reader returning &lt;code&gt;403&lt;/code&gt; to the default &lt;code&gt;python-urllib&lt;/code&gt; agent. Unit tests pass with a mocked network; only exercising the real endpoint surfaces that class of bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should you actually adopt?
&lt;/h2&gt;

&lt;p&gt;Start free and self-hosted, add metered tools only where you have measured a need. As of 2026-07 the stack I run is: self-hosted SearXNG behind a cache for search, a keyless Jina-backed reader CLI for fetch (run in a subagent so raw pages never hit the main context), a self-hosted crawler for bulk scraping, and local Playwright for the genuine browser work. Native provider search sits behind all of it as a metered last resort.&lt;/p&gt;

&lt;p&gt;The through-line is not a specific tool, it is two rules. &lt;strong&gt;Convert to markdown before the model sees a page&lt;/strong&gt;, which reclaims most of the token bill on day one. And &lt;strong&gt;offload the fetch to a subagent&lt;/strong&gt;, so the raw content, even cleaned, is summarized down before it touches the conversation the main agent has to keep coherent. Everything else is a cost-and-rate-limit optimization on top of those two moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Giving an agent web access is easy; giving it web access that does not quietly bankrupt its context window takes two deliberate choices. Extract to markdown, and read in a subagent. Do those, rank every tool by free-tier and rate cap before you reach for a paid API, and prefer a thin CLI over an MCP for anything that is not a stateful browser. The lazy path costs you a large slice of your web-read tokens, up to 80% on a boilerplate-heavy page, and a &lt;code&gt;429&lt;/code&gt; under load. The fix is a day of plumbing and a 120-line script, and it pays for itself the first time your agent reads the docs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.danmercede.com/guides/giving-your-agent-web-access" rel="noopener noreferrer"&gt;danmercede.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosting</category>
      <category>devops</category>
      <category>homelab</category>
      <category>docker</category>
    </item>
    <item>
      <title>The Reviewer Blessed the Bug: Why Agent-Written Code Needs Layered Review</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Sat, 04 Jul 2026 15:56:53 +0000</pubDate>
      <link>https://dev.to/danmercede/the-reviewer-blessed-the-bug-why-agent-written-code-needs-layered-review-2kl6</link>
      <guid>https://dev.to/danmercede/the-reviewer-blessed-the-bug-why-agent-written-code-needs-layered-review-2kl6</guid>
      <description>&lt;p&gt;A security reviewer read my diff and signed off on a specific claim: the credential-scrubbing was correct, "encoded access/secret forms are distinct, no overlap mis-redaction." It was thorough. It traced every log path. It was also wrong.&lt;/p&gt;

&lt;p&gt;One review layer later, a different reviewer took the same function, fed it two overlapping credentials, and printed the result: a &lt;strong&gt;partial secret sitting in the log line&lt;/strong&gt;. The redaction had eaten part of one secret and left the tail of another exposed. Same code, same afternoon, opposite verdict.&lt;/p&gt;

&lt;p&gt;That gap between "a careful reviewer certified this" and "an independent adversary broke it in one run" is the entire case for layered review. When an agent writes your code, and increasingly it does, no single review pass is a safety net. The passes have to be &lt;strong&gt;independent&lt;/strong&gt;, because independence is the only thing that makes their blind spots not line up.&lt;/p&gt;

&lt;p&gt;This guide walks the real arc: one small log-scrubber, four review layers, five defects, and the exact modality that let each layer catch what the others could not see. Every bug here is a shape you can copy into your own checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; anyone shipping code an AI agent wrote, or building the review harness that gates it, especially around self-hosted infrastructure where a quiet failure hides for weeks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What was actually being hardened?
&lt;/h2&gt;

&lt;p&gt;In a 2026-07 pass on a self-hosted LLM-observability stack (Langfuse on top of ClickHouse, with backups pushed to a MinIO object store and an encrypted off-box replica via restic), I hardened one function: a log scrubber that strips credentials out of backup-failure messages before they reach a cron log. Roughly 20 lines. It is exactly the kind of small, security-adjacent code an agent produces in seconds and a human skims in seconds.&lt;/p&gt;

&lt;p&gt;The scrubber matters because backup scripts fail loudly, and a failure message often contains the command that failed, which contains the credentials. If a nightly backup dies and dumps an S3 secret key into a log that ships to your aggregator, you have turned a reliability event into a credential-exposure event. The scrubber's whole job is to make that impossible.&lt;/p&gt;

&lt;p&gt;So it is a good test case: tiny, high-stakes, and the sort of thing everyone assumes is too small to need real review. It went through four layers anyway. Here is what each one saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does live-environment recon catch what no diff can?
&lt;/h2&gt;

&lt;p&gt;Before any diff review, I ran the backup once against the real host, and that single run exposed a defect no code reader could have found: the script selected its MinIO container with a fuzzy name match, and the host happened to run &lt;strong&gt;two&lt;/strong&gt; MinIO containers. The match grabbed the wrong one, hit Access Denied on a bucket it could not see, and reported a false CRITICAL every night while the actual backup was fine.&lt;/p&gt;

&lt;p&gt;No diff review catches this. The code was locally correct. &lt;code&gt;docker ps&lt;/code&gt; filtered by a name substring returns the first match, and on a one-MinIO dev box that first match is always the intended one. The bug only exists in the shape of the live environment, where a second, unrelated MinIO happened to sort ahead of the one the backup wanted. The lesson is old and keeps being true: &lt;strong&gt;a passing local test and a clean diff describe the code, not the world it runs in.&lt;/strong&gt; The fix was to select the container by exact name, defaulted in-script, so a fuzzy match can never wander onto the wrong instance.&lt;/p&gt;

&lt;p&gt;Recon is a review layer in its own right. It senses runtime state, a modality a static reviewer cannot reach. If your agent-review harness is all diff-readers, it stays blind to an entire class of defect that only appears on contact with production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does a pre-PR reviewer fleet catch that a single reviewer misses?
&lt;/h2&gt;

&lt;p&gt;Before the pull request opened, I ran three independent reviewers over the diff (adversarial, correctness, and security), and the security pass caught a redaction bug hiding in plain sight: the scrubber replaced each secret with a shell pattern substitution, &lt;code&gt;s="${s//${SECRET}/&amp;lt;redacted&amp;gt;}"&lt;/code&gt;, and the unquoted needle is treated as a &lt;strong&gt;glob&lt;/strong&gt;. A secret containing a bracket expression like &lt;code&gt;[ab]&lt;/code&gt; is read as a character class, fails to match itself literally, and passes through the log unredacted. A &lt;code&gt;*&lt;/code&gt; or &lt;code&gt;?&lt;/code&gt; in the secret fails the other way, over-matching and redacting adjacent log text. Both are wrong; only one leaks, and you cannot predict which without knowing the secret.&lt;/p&gt;

&lt;p&gt;The fix is one edit, quoting the needle so the match is always literal:&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;# unquoted: a bracket-expression secret leaks, and * or ? over-redacts&lt;/span&gt;
&lt;span class="nv"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SECRET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt;/&amp;lt;redacted&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# quoted: the secret is matched literally, both failure modes closed&lt;/span&gt;
&lt;span class="nv"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&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;SECRET&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;/&amp;lt;redacted&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value of the fleet is not three times the reading. It is three &lt;strong&gt;different&lt;/strong&gt; readings. Point one reviewer at a diff and you get one model's priors about what is worth checking. Point three with distinct adversarial framings and their blind spots do not fully overlap, so the union of what they notice is strictly larger. This is the cheapest layer to add and the one people skip most, because a single "looks good" from a capable model feels like enough. It is not enough. It is one sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did the post-push layer catch what the security reviewer had certified?
&lt;/h2&gt;

&lt;p&gt;With an access key of &lt;code&gt;ABC&lt;/code&gt; and a password of &lt;code&gt;ABCDEF&lt;/code&gt;, a post-push reviewer ran the already-certified scrubber on the line &lt;code&gt;password=ABCDEF&lt;/code&gt; and got back &lt;code&gt;password=&amp;lt;redacted&amp;gt;DEF&lt;/code&gt;: a partial secret in the log. The pre-PR security pass had checked this exact function for overlapping-secret bugs and signed off, "encoded access/secret forms are distinct, no overlap mis-redaction." A second model, running the code instead of reasoning about it, broke that certification in one run.&lt;/p&gt;

&lt;p&gt;Here is the whole bug. The scrubber redacted each secret in sequence. If a shorter secret is a substring of a longer one, the shorter replacement fires first, mutates the line, and the longer secret no longer matches itself, so its tail survives:&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;# ACCESS='ABC'  RESTIC='ABCDEF'&lt;/span&gt;
&lt;span class="nv"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&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;ACCESS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;/&amp;lt;redacted&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# 'password=ABCDEF' -&amp;gt; 'password=&amp;lt;redacted&amp;gt;DEF'&lt;/span&gt;
&lt;span class="nv"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&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;RESTIC&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;/&amp;lt;redacted&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# no longer matches; 'DEF' leaks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to stop treating the needles as an ordered list and redact them &lt;strong&gt;longest-first&lt;/strong&gt;, so no shorter needle can fragment a longer secret before that secret is matched whole:&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;# sort the needles by length, descending, then redact each literally&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;needles&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &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;readarray &lt;span class="nt"&gt;-t&lt;/span&gt; needles &amp;lt; &amp;lt;&lt;span class="o"&gt;(&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="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;needles&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print length"\t"$0}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&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="k"&gt;for &lt;/span&gt;n &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;needles&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;&lt;span class="nv"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&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;n&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;/&amp;lt;redacted&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Longest-first closes the substring case, where one secret sits wholly inside another. It does not close the rarer case of two distinct secrets that overlap in the log line, a suffix of one equal to a prefix of another; random credentials almost never do this, but if yours can, redact in a single combined pass instead of a loop. State the bound; do not sell the fix as total.&lt;/p&gt;

&lt;p&gt;This is the load-bearing section, so sit with what happened. A competent reviewer did not skip the overlap question. It &lt;strong&gt;considered the overlap question and answered it wrong&lt;/strong&gt;, because it reasoned about the encoded forms being distinct and missed that the raw forms could overlap. A second, independent adversary, running the function instead of reasoning about it, found the counterexample immediately. Certification by one reviewer is an argument. Reproduction by an independent one is a fact. When you can afford only one, prefer the layer that runs the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can a merge gate itself be a review layer?
&lt;/h2&gt;

&lt;p&gt;The last defect was not in the code at all, and the CI gate surfaced it: my new test used credential-shaped fixtures (values with an &lt;code&gt;AKIA&lt;/code&gt; prefix, the AWS access-key-ID shape), and a secret scanner flagged them and &lt;strong&gt;transitively blocked the merge&lt;/strong&gt;, even though that scanner was not one of the branch's required status checks. A second scanner, run on the identical fixtures, passed. The gate disagreed with itself.&lt;/p&gt;

&lt;p&gt;The trap is one layer of indirection. The scanner was not in the branch-protection required list, so it looked advisory. But a separate fail-closed meta-check that &lt;em&gt;was&lt;/em&gt; required carried its own internal list of checks it waits on, and the scanner was in it. So a "non-required" check gated the merge through a required one. Reading only the branch-protection settings would tell you the merge was clear. It was not.&lt;/p&gt;

&lt;p&gt;Two portable lessons fall out. First, &lt;strong&gt;never hardcode credential-shaped fixtures&lt;/strong&gt;; use obviously-fake, low-entropy placeholders that still exercise the code path, because a scanner cannot tell your test data from a live leak and should not have to. Second, when a merge is blocked and the required checks look green, &lt;strong&gt;read what your meta-gates actually wait on&lt;/strong&gt;, not just the protection rules. The scanner also scans full branch history, so neutralizing the fixtures at the tip does not clear a hit an earlier commit introduced; a squash-merge that collapses to the net diff does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why don't the layers just catch the same things?
&lt;/h2&gt;

&lt;p&gt;Across the arc, four layers caught five distinct defects with no overlap in the catches (the merge gate alone surfaced two, the credential-shaped fixtures and the transitive block), because each layer senses a different modality. Live recon senses runtime state. The pre-PR fleet senses the diff with fresh adversarial priors. The post-push pass senses by an independent model that runs the code. The CI gate senses policy and scanner state. Independent modalities have independent blind spots, which is the only reason stacking them pays off.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Review layer&lt;/th&gt;
&lt;th&gt;Sensing modality&lt;/th&gt;
&lt;th&gt;The defect it caught&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Live recon&lt;/td&gt;
&lt;td&gt;runtime state on the real host&lt;/td&gt;
&lt;td&gt;wrong container selected, false CRITICAL nightly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-PR reviewer fleet&lt;/td&gt;
&lt;td&gt;fresh adversarial read of the diff&lt;/td&gt;
&lt;td&gt;glob-unquoted redaction needle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-push pass&lt;/td&gt;
&lt;td&gt;an independent model that runs the code&lt;/td&gt;
&lt;td&gt;sequential-substitution partial-secret leak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI merge gate&lt;/td&gt;
&lt;td&gt;policy and scanner state&lt;/td&gt;
&lt;td&gt;credential-shaped fixtures, transitive merge block&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stacking identical reviewers does not work. Three passes from the same model with the same framing tend to miss the same things, because they share priors. The gain comes from &lt;strong&gt;orthogonality&lt;/strong&gt;: a runtime probe sees what a reader cannot, a code-runner sees what a reasoner cannot, a scanner sees what a designer forgot. The arc is a clean natural experiment for this, because one layer certified the precise property another layer broke. If the two had shared a modality, they would have shared the blind spot, and the leak would have shipped.&lt;/p&gt;

&lt;p&gt;The uncomfortable corollary for AI-written code: the model that wrote the code, the model that reviews the diff, and the model that reasons about correctness are correlated failure sources when they are the same model reasoning the same way. Independence is not a nice-to-have on top of review. Independence &lt;strong&gt;is&lt;/strong&gt; the review.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you build this without drowning in process?
&lt;/h2&gt;

&lt;p&gt;You do not need a 20-layer gauntlet on every one-line change; you need a few genuinely independent layers on anything security-adjacent, and a smaller set elsewhere. In practice, for agent-written infrastructure code, four layers cover the modalities that matter, and each is cheap once wired.&lt;/p&gt;

&lt;p&gt;Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run it against the real environment before you trust the diff.&lt;/strong&gt; One live execution surfaces the wrong-container, wrong-endpoint, and "works-on-the-dev-box" class that no reader catches. Recon is a layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan out a small pre-PR reviewer set with distinct framings,&lt;/strong&gt; not one reviewer three times. Adversarial, correctness, and a security pass on the named bug classes. The union beats the average.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add one post-push pass by a different model that runs the code,&lt;/strong&gt; not just reads it. This is the layer that catches what your best reviewer confidently blessed. Weight reproduction over certification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the CI gate as a reviewer and read its real dependency graph,&lt;/strong&gt; including what your fail-closed meta-checks wait on. And keep credential-shaped data out of tests entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The through-line is not "more review." It is &lt;strong&gt;independent&lt;/strong&gt; review. Five defects in one 20-line function, each invisible to every layer but one, is the normal shape of software when a fast writer meets a single slow reader. That is not a verdict on the agent; it is arithmetic. Give the writer four orthogonal readers instead, and the blind spots stop lining up.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.danmercede.com/guides/why-agent-code-needs-layered-review" rel="noopener noreferrer"&gt;danmercede.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>codereview</category>
      <category>security</category>
    </item>
    <item>
      <title>Building a Governed, Double-Send-Safe Delivery Pipeline for Agent Outputs</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Fri, 03 Jul 2026 22:17:35 +0000</pubDate>
      <link>https://dev.to/danmercede/building-a-governed-double-send-safe-delivery-pipeline-for-agent-outputs-80e</link>
      <guid>https://dev.to/danmercede/building-a-governed-double-send-safe-delivery-pipeline-for-agent-outputs-80e</guid>
      <description>&lt;p&gt;I built a multi-agent system to run a small business. Agents drafted work, and some of that work left the building: emails to real people, exported documents, delivered artifacts. I later retired the business on market grounds, but the delivery pipeline is the piece I would rebuild without changing a line. It answers a question most agent demos skip: how do you let an autonomous system send things on a person's behalf without ever double-sending, misfiring, or shipping something nobody approved?&lt;/p&gt;

&lt;p&gt;This guide is the pattern, generalized and stripped of the business it ran. It is for anyone whose agent sends consequential output: customer email, outbound messages, exported records, anything where "sent twice" or "sent without sign-off" is a real cost rather than a cosmetic bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one-line version:&lt;/strong&gt; treat every outbound action as a durable, leased, fail-closed transaction with a human approval gate and an evidence receipt, and make the crash-recovery path refuse to re-send by default. The rest of this guide is that sentence, unpacked.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why does an agent that sends email send it twice?
&lt;/h2&gt;

&lt;p&gt;The double-send is not a rare edge case; it is the default behavior of a naive queue the first time a worker dies mid-send. A worker claims a send task, hands the message to the mail server, and then is killed (out of memory, a deploy, a reboot) before it writes "done." The task still looks unfinished, so the next worker picks it up and sends again. The customer gets two emails and you get a support ticket.&lt;/p&gt;

&lt;p&gt;Every safe-delivery decision below exists to close that window. The failure is structural, not bad luck: the moment of maximum danger is the gap between "the side effect happened in the outside world" and "the system recorded that it happened." A crash in that gap is when a retry becomes a duplicate. You cannot make the gap zero, so you design for a crash inside it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What does the governed delivery pipeline look like?
&lt;/h2&gt;

&lt;p&gt;The pipeline composes into one spine, and the rule is that no consequential send skips the approval, single-send, and attest gates. The score and review stages decide which items a human reads first rather than gating every send. Six stages, each durable before the next begins:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Produce.&lt;/strong&gt; The agent generates the artifact in full (for email, a complete RFC 5322 message) and stages it. Nothing is sent inline at generation time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persist.&lt;/strong&gt; Write the intent (the staged artifact plus a queue row and a receipt) to durable storage before anything leaves the process. If you crash now, the intent survives and is inspectable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score.&lt;/strong&gt; Attach a confidence score to the output. This is the input to review routing, not a gate by itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review.&lt;/strong&gt; Route low-confidence items to a human review queue. High-confidence items still do not bypass the next stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve.&lt;/strong&gt; A fail-closed approval gate. The release is blocked by default and proceeds only on an explicit, recorded authorization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send and attest.&lt;/strong&gt; Send exactly once under a lease, then reconcile and write an evidence receipt describing what happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important property is that steps 2 through 6 are separate durable transitions, not one function call. Each stage can crash and resume without losing or duplicating work, because the state that matters lives in the store, not in a worker's memory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fgoverned-double-send-safe-delivery%2Fgoverned-delivery-spine.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fgoverned-double-send-safe-delivery%2Fgoverned-delivery-spine.webp" title="Six durable stages; each arrow is its own transaction, so a crash between stages never loses or duplicates work." alt="The governed delivery spine: six durable stages (produce, persist, score, review, approve, and send with attest), with low-confidence items branching to a human review queue and each arrow a separate durable transition." width="799" height="284"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you make the send idempotent when a worker dies?
&lt;/h2&gt;

&lt;p&gt;This is the stage most teams get wrong, and the fix is a rule, not a library. Use row-level leasing so exactly one worker owns a task at a time, and make external-send actions non-requeueable on lease expiry. In Postgres the leasing primitive is a claim under &lt;code&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/code&gt;: concurrent workers select different rows, never the same one, so two workers cannot both send.&lt;/p&gt;

&lt;p&gt;The lease is what makes crash recovery safe, and the reaper is where the real decision lives. A reaper reclaims tasks whose lease expired because the worker died. For ordinary internal work, reclaiming and re-running is correct. For an external send it is a loaded gun: if the worker died after the mail server accepted the message but before it recorded success, re-running sends a duplicate. So the rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An external-send action is non-requeueable on lease expiry. When its lease dies, the reaper strands the task for human reconciliation instead of re-running it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the whole trick. A visible stuck task beats an invisible double-send. You trade an automatic recovery you cannot prove is safe for a manual one a human can resolve in seconds by checking whether the message actually went out. Internal, side-effect-free tasks stay auto-requeueable; only the actions that touch the outside world are stranded. Encode the action type in the task and let the reaper branch on it; do not rely on a human remembering the distinction.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fgoverned-double-send-safe-delivery%2Fdouble-send-safe-reaper.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fgoverned-double-send-safe-delivery%2Fdouble-send-safe-reaper.webp" title="The recovery path branches on action type: internal tasks requeue, external sends strand for a human." alt="The double-send-safe reaper: a worker claims a task under a lease and dies mid-send; when the lease expires the reaper branches on action type, requeuing internal tasks but stranding external sends for human reconciliation to avoid a duplicate customer send." width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you keep the pipeline fail-closed?
&lt;/h2&gt;

&lt;p&gt;Fail-closed means the safe state is the default and every unsafe action requires an explicit, positive signal to proceed. Three defaults carry most of the weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sending is off until switched on.&lt;/strong&gt; A single delivery-enabled flag gates all outbound sends, defaulting to off. A misconfiguration, a fresh environment, or a half-deployed release sends nothing rather than sending wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The sender identity is checked, not assumed.&lt;/strong&gt; At the point of send, the from-address is checked against a verified sending domain, and a transport guard refuses to send if encrypted transport is expected but the connection would fall back to an unencrypted or wrong port. These checks live in send code, not in a config comment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing consequential ships without a recorded approval, and everything that ships leaves a receipt.&lt;/strong&gt; The approval gate is the choke point; the receipt is the audit trail. A stranded, unreconciled send is deliberately left without a success receipt, because its outcome is genuinely unknown until a human checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A short operational note, because it cost me. A swallowed import error once left a preflight check silently broken for about 65 days; a reboot finally surfaced it, and the whole time the failure had been read as a benign "dependency unavailable" state. A suppressed error that reads as a normal state is its own failure mode. Fail-closed only helps if the closed state is loud.&lt;/p&gt;




&lt;h2&gt;
  
  
  When do you actually need this, and when is it overkill?
&lt;/h2&gt;

&lt;p&gt;Match the machinery to the blast radius of a wrong send. For low-stakes internal output (a draft in a channel, a log line, a suggestion a human will obviously see and fix), this full pipeline is overkill and you should not build it. The cost of the ceremony exceeds the cost of the mistake.&lt;/p&gt;

&lt;p&gt;You need it the moment a wrong or duplicate send has real cost: customer-facing email, anything touching money, anything with a compliance or legal record, anything a recipient acts on. For those, the pipeline is the floor, not the gold-plating. The tell is simple: if "sent twice" or "sent without sign-off" would generate a support ticket, a refund, or a liability, you are in scope.&lt;/p&gt;

&lt;p&gt;The pattern outlived the business I built it for, which is the honest reason it is here rather than in a product. The decision to retire that system, and the principle that a clean architecture is a reason to document the pattern rather than keep funding the business, is its own argument, made in the companion essay "Architecture Fit Is Not Business Justification." Build the pipeline when the sends are consequential. Skip it when they are not. And whatever you build, make the reaper refuse to re-send by default.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.danmercede.com/guides/governed-double-send-safe-delivery" rel="noopener noreferrer"&gt;danmercede.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>llm</category>
      <category>ai</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Alibi of a Green Gate</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Tue, 30 Jun 2026 00:28:56 +0000</pubDate>
      <link>https://dev.to/danmercede/the-alibi-of-a-green-gate-3g6m</link>
      <guid>https://dev.to/danmercede/the-alibi-of-a-green-gate-3g6m</guid>
      <description>&lt;p&gt;A control is what it does to the live target, not what it is declared to be. We build the gate, the reaper, the required check, the backup. We arm it, it runs, it exits zero, the dashboard is green. Then the leak it was meant to kill is still leaking, the bad merge it was meant to refuse already landed, the stale image it was meant to replace is still serving months-old code. Present, configured, armed, inert. None of those four facts is the one that matters.&lt;/p&gt;

&lt;p&gt;The failure has one shape, and it is structural. The control's selection model, its liveness model, or its notion of "done" does not match the world it governs, so it sweeps an empty set, or whitelists by naming the bad states it knows and silently passing the ones it does not, or inspects the merged PR instead of the running artifact. Nothing throws. A no-op and a success produce the identical green. The green proves the control ran; it says nothing about whether it bit.&lt;/p&gt;

&lt;p&gt;That is the whole trap: "armed and error-free" reads exactly like "remediating," and the reading is free while the proof costs a probe. So pay for the probe. The question is never "did it run." It is "did anything in the live world change because it fired": did the leak shrink, did the merge get refused, did the served bytes turn over. A control that cannot point at an effect on its target has not earned the word "working." Trust the effect, never the exit code. A green gate over an inert control is theater. Prove it bit.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.danmercede.com/thoughts/2026-06-29-the-alibi-of-a-green-gate" rel="noopener noreferrer"&gt;danmercede.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>architecture</category>
      <category>programming</category>
    </item>
    <item>
      <title>Coherent Isn't Correct</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Mon, 29 Jun 2026 00:30:00 +0000</pubDate>
      <link>https://dev.to/danmercede/coherent-isnt-correct-a97</link>
      <guid>https://dev.to/danmercede/coherent-isnt-correct-a97</guid>
      <description>&lt;h2&gt;
  
  
  Coherent Isn't Correct
&lt;/h2&gt;

&lt;p&gt;Experiment Log · 2026-06-28 · 05:30 PM PT&lt;/p&gt;

&lt;p&gt;Hypothesis: Hand an autonomous AI coding agent a whole local-first finance tool to build end-to-end, no human writing code, and the output will be safe to use as-is.&lt;/p&gt;

&lt;p&gt;Constraint: First pass fully autonomous; then an independent audit and a harden loop where every finding becomes a failing test before it's fixed; every public claim must be backed by code.&lt;/p&gt;

&lt;p&gt;Result: Failed. The agent (Grok) shipped something genuinely coherent — an installable, Dockerized, CI-green vertical with a CLI, a web UI, a sqlite ledger, invoices, and reports. But it was broken in exactly the parts that matter for money: amounts stored as floats (the Markdown and JSON reports disagreed), a fabricated 42-dollar charge recorded on a receipt it couldn't read, a parser that truncated 1,234.56 down to 1.23, a 'CSV export' that emitted JSON with no OFX at all, an 'optional local vision' feature with zero code behind it, and a file upload with a path-traversal hole that ran as root. The harden loop — a self-adversarial review plus three independent automated review passes, each finding turned into a failing test — caught a critical data-loss bug in the upgrade path and fixed the rest. v0.2 now uses exact integer money, flags unreadable receipts for review instead of inventing amounts, ships real CSV and OFX exports, binds loopback as a non-root container, and has 85 tests at 91 percent coverage.&lt;/p&gt;

&lt;p&gt;Next step: Treat an autonomous build the way you'd treat a junior pull request for a system that touches money: audit it adversarially and make every claim earn a test. The agent is great at producing something that looks shipped; the unglamorous correctness work is still yours. The build is cheap — the harden loop is the product.&lt;/p&gt;

&lt;p&gt;Tags: #failure-modes #execution #systems&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>testing</category>
    </item>
    <item>
      <title>Placement Beats Rules</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Sun, 28 Jun 2026 21:40:00 +0000</pubDate>
      <link>https://dev.to/danmercede/placement-beats-rules-14p6</link>
      <guid>https://dev.to/danmercede/placement-beats-rules-14p6</guid>
      <description>&lt;h2&gt;
  
  
  Placement Beats Rules
&lt;/h2&gt;

&lt;p&gt;Short Essay · 2026-06-28 · 02:40 PM PT&lt;/p&gt;

&lt;p&gt;Claim: Most agent-instruction files already encode the popular best-practice rules. The failures that remain are rarely a missing rule — they are a rule that never loads and a claim that was never verified.&lt;/p&gt;

&lt;p&gt;A popular checklist of rules for agent-instruction files made the rounds again this week — think before coding, keep it simple, surgical changes, define success criteria, and a newer set about reproducing bugs with a test, debugging to root cause, and keeping a list of named failure modes the agent should self-check against.&lt;/p&gt;

&lt;p&gt;The rules are good. But measure your own setup against them honestly and you may find the same thing I did: the file already encodes nine of ten. The gap isn't the rules. It's two things no rule list mentions.&lt;/p&gt;

&lt;p&gt;**1. A failure-mode list only works if it's always loaded.** "Keep a list of named failure modes" is excellent advice that quietly assumes the list is in the context the model reads every turn. If yours lives in a separate notes store the agent has to search, it gets checked exactly when the agent thinks to search it — which is the opposite of when you need it. The recurring traps belong inline, in the always-loaded file, not one retrieval hop away.&lt;/p&gt;

&lt;p&gt;**2. Context files truncate silently.** The always-loaded memory index I lean on was being injected *truncated* — its tail, which held a chunk of those "recall" lessons, was getting dropped above a size cap, and the harness only whispered it in a line that's easy to miss. Worse: a note *inside* that file confidently asserted the cap "isn't enforced," verified weeks earlier. Two failure modes stacked — a real limit, and a stale note swearing there wasn't one. The fix was small once seen: prove what the model actually received (it differs from what's on disk once you cross the cap), move the highest-value lessons to the top so the tail is what drops, and correct the note that lied.&lt;/p&gt;

&lt;p&gt;The meta-lesson is the cheap one: a rule you adopt is only as good as its placement and your verification of it. Adopting the rule feels like progress. Confirming it loads, and re-checking the claims your own files make about themselves, is the part that actually moves the error rate.&lt;/p&gt;

&lt;p&gt;Audit the two things the rule lists skip: is your recurring-failure list in always-loaded context or behind a retrieval step the agent only hits when it remembers to; and does your context file silently truncate above a size cap. Verify what the model actually loaded, keep the highest-value lessons at the top, and trust your own old notes least when they contradict live behavior.&lt;/p&gt;

&lt;p&gt;Tags: #failure-modes #execution #systems&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>debugging</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Self-Correcting Agents: Learning the Loop the Hard Way</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Sat, 27 Jun 2026 17:12:52 +0000</pubDate>
      <link>https://dev.to/danmercede/when-your-verifier-goes-quiet-why-a-crashed-reviewer-is-not-a-refutation-5bnm</link>
      <guid>https://dev.to/danmercede/when-your-verifier-goes-quiet-why-a-crashed-reviewer-is-not-a-refutation-5bnm</guid>
      <description>&lt;p&gt;I ran a multi-agent research agent over a hard question and it came back with a clean, confident verdict: &lt;strong&gt;"All 25 claims refuted by adversarial verification. Research inconclusive."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every one of those 25 claims was true. Several cited real, recent papers I could pull up in another tab. Nothing had actually been checked.&lt;/p&gt;

&lt;p&gt;The verifier hadn't &lt;em&gt;disagreed&lt;/em&gt; with the research. It had &lt;strong&gt;crashed&lt;/strong&gt; — every verification call hit transient API rate-limiting and returned nothing — and the loop scored "returned nothing" as "refuted." A fail-closed gate that cannot tell &lt;em&gt;refuted by evidence&lt;/em&gt; apart from &lt;em&gt;never adjudicated&lt;/em&gt; will lie to you with total confidence, and dress the lie up as diligence.&lt;/p&gt;

&lt;p&gt;This guide is about that failure mode: why it happens, why it's the &lt;em&gt;default&lt;/em&gt; outcome unless you design against it, how to spot it in your own loops, the gate that prevents it, and how to recover a run that has already produced a false "everything is false."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; anyone building agent loops that check their own work — research agents, code agents with a review pass, anything with a "generate → verify → keep or discard" cycle. You don't need a specific framework. You need to take one idea seriously: &lt;strong&gt;silence is not a verdict.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The anatomy of a self-checking loop
&lt;/h2&gt;

&lt;p&gt;Almost every serious agent loop has the same three roles, whatever you call them. The reasoning-frontiers survey of LLM reasoning fixes the vocabulary (arXiv:2504.09037):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Reasoner&lt;/strong&gt; generates candidate answers (the policy).&lt;/li&gt;
&lt;li&gt;One or more &lt;strong&gt;Verifiers&lt;/strong&gt; evaluate them (the judge / reward signal).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Refiner&lt;/strong&gt; revises based on verifier feedback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three can be the same model wearing different hats — that's exactly what "self-refinement" is. The research agent that lied to me was a fan-out version of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scope ──► search ──► fetch ──► extract claims ──► VERIFY (3-vote panel per claim) ──► synthesize
                                                      │
                                                      └─ each claim needs a quorum of votes to survive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verify stage is the load-bearing one. It's where the loop decides what's &lt;em&gt;true enough to keep&lt;/em&gt;. And it's the stage everyone gets wrong, because it's the stage where the failure is invisible: a broken Reasoner produces obvious garbage, but a broken &lt;strong&gt;Verifier&lt;/strong&gt; produces clean, plausible, wrong verdicts.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The bug: a two-state gate in a three-state world
&lt;/h2&gt;

&lt;p&gt;Here's the gate that shipped in the loop that failed me. Each claim got three independent verifier votes, tallied as &lt;code&gt;notRefuted–refuted&lt;/code&gt;. The survive rule was reasonable on its face:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A claim survives if it has &lt;strong&gt;at least 2 valid votes&lt;/strong&gt; and &lt;strong&gt;fewer than 2 refutations.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that rule carefully. It quietly assumes votes &lt;em&gt;exist&lt;/em&gt;. When the verify phase crashed under rate-limiting, every vote came back empty. Each claim scored &lt;strong&gt;&lt;code&gt;0–0&lt;/code&gt;&lt;/strong&gt;: zero refutations, but also zero valid votes. The survive rule said &lt;em&gt;"fewer than 2 refutations? yes. At least 2 valid votes? no."&lt;/em&gt; — so the claim didn't survive.&lt;/p&gt;

&lt;p&gt;So far, so correct. &lt;strong&gt;Refusing to pass an unverified claim is good design.&lt;/strong&gt; The bug was one layer up, in the &lt;em&gt;reporting&lt;/em&gt;: the claims that didn't survive were dumped into a bucket literally named &lt;code&gt;refuted[]&lt;/code&gt;, and the summary rendered that bucket as &lt;em&gt;"all 25 claims refuted, research inconclusive."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The gate had collapsed a &lt;strong&gt;three-state world into two states:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fverifier-abstention%2Fthree-verdict-model.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fverifier-abstention%2Fthree-verdict-model.webp" title="A verifier vote has three outcomes, not two. Folding ABSTAINED into REFUTED is how a crash becomes a confident 'no.'" alt="A claim's verifier outcome has three states — CONFIRMED, REFUTED, and ABSTAINED — but a naive gate sorts every claim into just CONFIRMED or REFUTED, dropping ABSTAINED silently into the refuted bucket." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The world has &lt;strong&gt;three&lt;/strong&gt; outcomes, and you must keep them distinct:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CONFIRMED&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;quorum of valid votes, below the refute threshold&lt;/td&gt;
&lt;td&gt;keep it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;REFUTED&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;quorum of valid votes, at/above the refute threshold&lt;/td&gt;
&lt;td&gt;drop it — &lt;em&gt;the evidence says no&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ABSTAINED&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;below quorum of &lt;em&gt;valid&lt;/em&gt; votes (crash, timeout, null, rate-limit)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;you don't know yet&lt;/strong&gt; — re-run, escalate, or surface as pending&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The one rule this whole guide rests on:&lt;/strong&gt; an absence of votes is not a vote. &lt;em&gt;Refuted&lt;/em&gt; is a verdict the evidence reached; &lt;em&gt;abstained&lt;/em&gt; is a verdict the evidence never reached. A gate that conflates them converts every transient outage into a wall of false negatives — and because it fails &lt;em&gt;closed&lt;/em&gt;, it looks responsible while doing it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Why this is the default, not an edge case
&lt;/h2&gt;

&lt;p&gt;You might think this is an exotic bug. It isn't — it's what you get &lt;em&gt;unless you specifically prevent it&lt;/em&gt;, for three compounding reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fail-closed is the right instinct, and that's the trap.&lt;/strong&gt; You &lt;em&gt;should&lt;/em&gt; refuse to pass a claim you couldn't verify. Every safety-conscious engineer builds the gate to deny on uncertainty. The problem is that "deny" and "refute" land in the same bucket unless you force them apart. The safer your instinct, the more likely you are to mislabel silence as rejection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verifiers fail in correlated bursts.&lt;/strong&gt; A single flaky vote is noise you'd shrug off. But verifier calls share infrastructure — the same API, the same rate limiter, the same network. When one abstains from a transient limit, they &lt;em&gt;all&lt;/em&gt; do, at once. So you don't get one mislabeled claim; you get &lt;em&gt;the whole run&lt;/em&gt; mislabeled in a single stroke. The blast radius is the entire output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The summary layer launders the error.&lt;/strong&gt; The gate's internal state might still be technically recoverable (&lt;code&gt;0–0&lt;/code&gt; is visibly different from &lt;code&gt;0–2&lt;/code&gt;). But by the time it reaches a one-line summary — &lt;em&gt;"research inconclusive"&lt;/em&gt; — the distinction is gone. A human reading the summary has no way to tell a crash from a conclusion. The error becomes load-bearing for every downstream decision.&lt;/p&gt;

&lt;p&gt;This is a specific, nasty instance of a general truth the research keeps surfacing: &lt;strong&gt;agents are far better at &lt;em&gt;producing&lt;/em&gt; a good answer than at &lt;em&gt;recognizing&lt;/em&gt; one.&lt;/strong&gt; The verification step is the weakest link in the loop, so it deserves the most defensive design — not the least.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The research: verification is the bottleneck, not generation
&lt;/h2&gt;

&lt;p&gt;Step back from the bug and look at why the &lt;em&gt;verify&lt;/em&gt; stage is where loops live or die.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The verification gap.&lt;/strong&gt; A 2026 benchmark of general LLM agents put a name to the failure (arXiv:2602.18998): when you sample several candidate answers, the &lt;em&gt;right&lt;/em&gt; one is often already in your set — your pass@K is high, because the model &lt;em&gt;generated&lt;/em&gt; it — but the model &lt;strong&gt;can't reliably select it&lt;/strong&gt;, so the accuracy you actually realize lags far behind the answer you already have. Generation outruns judgment. This is the structural reason "just let the agent double-check itself" underdelivers, and the reason verifier quality dominates loop quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The self-correction blind spot.&lt;/strong&gt; It gets sharper. &lt;em&gt;Self-Correction Bench&lt;/em&gt; measured an average &lt;strong&gt;64.5% blind-spot rate across 14 models&lt;/strong&gt; (arXiv:2507.02778): models readily fix an error when it's pointed out in the &lt;em&gt;prompt&lt;/em&gt;, but systematically fail to fix the &lt;em&gt;same&lt;/em&gt; error in &lt;em&gt;their own&lt;/em&gt; prior output. The likely cause is mundane — training data is overwhelmingly composed of clean, correct demonstrations, not error-then-correction traces, so the "notice and revise my own mistake" behavior is under-trained. The striking part: &lt;strong&gt;simply appending the word "Wait" cut blind spots by 89.3%.&lt;/strong&gt; The capability is there; it just doesn't fire on its own.&lt;/p&gt;

&lt;p&gt;Put those two findings together and the design conclusion is blunt: &lt;strong&gt;a loop that relies on a single model to silently self-judge is building on the part of the system that is measurably worst at the job.&lt;/strong&gt; You cannot fix that with more self-reflection. You fix it with &lt;em&gt;more, and more independent, verification&lt;/em&gt; — which is exactly why a verifier that quietly drops to zero is so destructive. It removes the one thing that was compensating for the model's blind spot, and it does so invisibly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification is its own scaling axis.&lt;/strong&gt; The frontier response to the verification gap is to &lt;strong&gt;scale the number of verifiers&lt;/strong&gt;, not the cleverness of one. Multi-Agent Verification (arXiv:2502.20379) uses off-the-shelf models as independent "Aspect Verifiers" that vote — no training required — and shows &lt;em&gt;weak-to-strong generalization&lt;/em&gt;: a panel of weaker verifiers can improve even a stronger generator. Rubric-grounded test-time verification (DeepVerifier, arXiv:2601.15808) extends the same idea to research agents, plug-and-play with no fine-tuning. The whole point of these systems is to make the verdict &lt;em&gt;robust&lt;/em&gt;. A panel that silently abstains &lt;em&gt;en masse&lt;/em&gt; is the precise opposite — it's a quorum that quietly dropped below quorum without telling anyone.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The fix: design the gate around three states
&lt;/h2&gt;

&lt;p&gt;The fix is not subtle once you've named the problem. &lt;strong&gt;Make ABSTAINED a first-class state and never let it masquerade as either pass or fail.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three rules implement it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 1 — classify on &lt;em&gt;valid&lt;/em&gt; votes, not on net score.&lt;/strong&gt; A vote only counts toward a verdict if the verifier actually ran and returned a parseable judgment. Before you tally &lt;code&gt;notRefuted&lt;/code&gt; vs &lt;code&gt;refuted&lt;/code&gt;, count how many votes are &lt;em&gt;valid at all&lt;/em&gt;. If valid votes are below your quorum, the outcome is &lt;code&gt;ABSTAINED&lt;/code&gt;, full stop — you never even reach the refute comparison.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for each claim:
    valid   = votes that actually ran and parsed
    refuted = valid votes that say "refuted"
    if len(valid) &amp;lt; QUORUM:                -&amp;gt; ABSTAINED   (not adjudicated)
    elif len(refuted) &amp;gt;= REFUTE_THRESHOLD: -&amp;gt; REFUTED     (evidence says no)
    else:                                  -&amp;gt; CONFIRMED    (keep)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;For the 3-vote panel from §2, &lt;code&gt;QUORUM = 2&lt;/code&gt; and &lt;code&gt;REFUTE_THRESHOLD = 2&lt;/code&gt; — the very same thresholds as the broken gate. Nothing about the numbers changed; only the **order&lt;/em&gt;* of the checks and the &lt;strong&gt;keying on valid-vote count&lt;/strong&gt; did.*&lt;/p&gt;

&lt;p&gt;The single change from the broken gate is the &lt;strong&gt;first branch runs first&lt;/strong&gt; and is keyed on &lt;em&gt;valid-vote count&lt;/em&gt;, not on the refutation count. A &lt;code&gt;0–0&lt;/code&gt; claim can never be reported as refuted because it exits at &lt;code&gt;ABSTAINED&lt;/code&gt; before the refutation branch is ever evaluated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 2 — an error, timeout, or null is an abstention, and abstentions get retried, never counted clean.&lt;/strong&gt; A verifier invocation that throws, times out, or returns nothing is a &lt;code&gt;PENDING&lt;/code&gt; abstention — re-run it up to &lt;em&gt;N&lt;/em&gt; times with backoff. Critically, an abstention is &lt;strong&gt;never&lt;/strong&gt; silently treated as a pass &lt;em&gt;or&lt;/em&gt; a fail. (This is the same discipline a good code-review gate needs: a reviewer subagent that errors out hasn't &lt;em&gt;approved&lt;/em&gt; the diff — treating "the review crashed" as "the review is clean" is the same bug wearing a different shirt.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fverifier-abstention%2Fabstention-tolerant-gate.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fverifier-abstention%2Fabstention-tolerant-gate.webp" title="The retry loop sits between 'vote arrived' and 'verdict counted.' Exhausted retries surface as PENDING, never as pass or refute." alt="An abstention-tolerant gate: a verifier vote is first checked for validity; invalid or missing votes route to a retry-up-to-N loop, and only a genuine quorum of valid votes produces a CONFIRMED or REFUTED verdict, while exhausted retries surface as PENDING — never as clean." width="799" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule 3 — never transcribe "inconclusive" as a finding without checking whether the verifiers ran.&lt;/strong&gt; This is the reporting-layer rule, and it's the one that actually saved me. Before a summary says &lt;em&gt;"all refuted"&lt;/em&gt; or &lt;em&gt;"inconclusive,"&lt;/em&gt; it must inspect the &lt;em&gt;shape&lt;/em&gt; of the votes that produced that verdict. If the run is dominated by &lt;code&gt;0–0&lt;/code&gt; abstentions, the honest summary is &lt;strong&gt;"verification did not complete"&lt;/strong&gt; — an operational failure to retry — not &lt;strong&gt;"the claims are false,"&lt;/strong&gt; a substantive finding. A crashed adjudicator's silence must never be rendered as a conclusion.&lt;/p&gt;

&lt;p&gt;A compact way to enforce Rule 3 is a tiny, &lt;em&gt;report-only&lt;/em&gt; reclassifier that you can run over any saved run output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reclassify(run_output):
    for each claim in run_output.refuted_bucket:
        valid = parse_vote_tally(claim)
        if valid &amp;lt; QUORUM:  -&amp;gt;  ABSTAINED   (recoverable; the run never judged it)
        else:               -&amp;gt;  REFUTED     (a real verdict)
    if ABSTAINED dominates:  print "ABSTENTION-DOMINATED — the run did not refute anything"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(One naming note so the labels don't trip you: &lt;strong&gt;ABSTAINED&lt;/strong&gt; is the terminal "not adjudicated" state — the same one a run's output later shows as "unverified"; &lt;strong&gt;PENDING&lt;/strong&gt; is just its mid-retry sub-state. One concept, named once.) It changes nothing and runs nothing — it just re-reads the tallies and tells you whether your "all refuted" was a verdict or a crash. That single check is the difference between trusting a false negative and catching it.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Detecting it in a loop you already have
&lt;/h2&gt;

&lt;p&gt;You can audit an existing loop for this failure without rebuilding it. The tells:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;What it usually means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A verify/review stage reports &lt;strong&gt;everything&lt;/strong&gt; failed at once&lt;/td&gt;
&lt;td&gt;correlated abstention (shared rate limit / outage), not a real mass rejection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verdicts flip between runs with no input change&lt;/td&gt;
&lt;td&gt;the gate is scoring transient failures as outcomes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The "failed" bucket has no &lt;em&gt;per-item evidence&lt;/em&gt; (no refuting quote, no reason)&lt;/td&gt;
&lt;td&gt;those items were never adjudicated — there's nothing to cite because nothing ran&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A summary says "inconclusive" but the underlying items cite real, checkable sources&lt;/td&gt;
&lt;td&gt;the loop discarded true results; verification, not the research, is what failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tightening a rate limit or shrinking a batch makes the "failure rate" drop&lt;/td&gt;
&lt;td&gt;you're measuring infrastructure, not correctness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The deepest tell is the absence of &lt;em&gt;evidence&lt;/em&gt; attached to a negative verdict. A genuine refutation can point at &lt;em&gt;why&lt;/em&gt; — a contradicting source, a failing assertion, a counterexample. An abstention can't, because nothing happened. &lt;strong&gt;If your "no" can't show its work, it isn't a no.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Recovery: resume, don't re-run
&lt;/h2&gt;

&lt;p&gt;Say it already happened — a run produced a false "everything refuted." The instinct is to run the whole expensive pipeline again from scratch — and in a real fan-out (mine spanned several stages and a couple of dozen sources), "from scratch" is a lot of wasted minutes and tokens. Don't. Most of that run is &lt;em&gt;fine&lt;/em&gt; and recoverable. Recover in three steps, cheapest first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fverifier-abstention%2Frecovery-flow.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.danmercede.com%2Fassets%2Fguides%2Fverifier-abstention%2Frecovery-flow.webp" title="Recovery is cheapest-first: reclassify (seconds) → resume from cache (only the crashed calls re-run) → sequential top-up for the tail." alt="Recovery flow: first reclassify the saved output to confirm the failure was abstention-dominated, then resume the run from its cached prefix so only the crashed verifier calls re-run, then do a gentle sequential top-up for any remaining tail." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — reclassify (seconds, changes nothing).&lt;/strong&gt; Run the report-only reclassifier from §5 over the saved output. It splits the "refuted" pile into &lt;em&gt;genuinely refuted&lt;/em&gt; (a real quorum said no) versus &lt;em&gt;abstained&lt;/em&gt; (below quorum — recoverable). If it comes back &lt;strong&gt;abstention-dominated&lt;/strong&gt;, you now know the research itself is intact; only the verification failed. You've turned a false catastrophe into a known, bounded gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — resume from the cached prefix; don't restart.&lt;/strong&gt; A good loop harness caches completed stages. The scope, the search, the fetch, and &lt;em&gt;every verifier vote that actually succeeded&lt;/em&gt; are still valid — re-running them wastes time and burns the same rate limit that caused the failure. Resume the run so that &lt;strong&gt;only the abstained verifier calls re-execute.&lt;/strong&gt; You pay for the gap, not the whole pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — top up the tail by hand, gently.&lt;/strong&gt; For the handful of items still unverified after the resume, verify them sequentially — one source at a time, at a calm cadence. The shared rate limiter that caused the crash &lt;em&gt;eases over time&lt;/em&gt;; a gentle, serial top-up slips under it where a parallel burst re-trips it. This is slow on purpose. Slow-and-complete beats fast-and-false.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on "future-dated" citations:&lt;/strong&gt; when you re-verify, resist the urge to auto-reject a source just because it looks improbable — a paper with a publication date a few months out, say. In my run, several "suspicious" citations resolved to entirely real papers on inspection. &lt;em&gt;Existence-check before you refute.&lt;/em&gt; That, too, is the difference between a verdict and a guess.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  8. The broader principle: spend compute on generation and external verification
&lt;/h2&gt;

&lt;p&gt;Zoom all the way out. The abstention bug is a sharp instance of a general lesson about &lt;em&gt;where inference compute pays off&lt;/em&gt; in loops — and the evidence is increasingly clear that the popular instinct is backwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brute repetition is the weak lever; verification is the binding constraint.&lt;/strong&gt; Spending more compute by sampling several independent trajectories and selecting among them &lt;em&gt;can&lt;/em&gt; help — the systematic study of test-time scaling for agents finds that scaling test-time compute, diversifying rollouts, and list-wise verification all move the needle (arXiv:2506.12928). But the payoff is far from automatic: a 2026 benchmark of &lt;em&gt;general&lt;/em&gt; LLM agents found that &lt;strong&gt;neither&lt;/strong&gt; parallel sampling &lt;strong&gt;nor&lt;/strong&gt; longer sequential reasoning reliably improved performance — sequential scaling hit an &lt;strong&gt;"effective context ceiling,"&lt;/strong&gt; and parallel scaling was capped by &lt;strong&gt;the verification gap itself&lt;/strong&gt; (arXiv:2602.18998). Read that twice: the thing throttling parallel sampling is the &lt;em&gt;same&lt;/em&gt; selection problem this whole guide is about. The right answer is often already in your samples; the agent just can't pick it — and letting one agent simply "think longer" is the weakest lever of all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reflect selectively, not constantly.&lt;/strong&gt; When you do refine, trigger it &lt;em&gt;only&lt;/em&gt; when a step's score falls below a threshold, rather than reflecting at every step — knowing &lt;em&gt;when&lt;/em&gt; to reflect beats reflecting often (arXiv:2506.12928). And self-verify-then-correct loops (SETS, arXiv:2501.19306) keep improving with compute where plain best-of-N plus majority vote saturates. The throughline: &lt;strong&gt;structured selectivity and stronger verification scale; brute repetition doesn't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now connect that back to the bug. If parallel generation puts the right answer in your candidate set, and the verification gap means selecting it is the hard part, then your &lt;strong&gt;verifier is the highest-leverage component in the entire loop.&lt;/strong&gt; Which is exactly why a verifier that can silently fall to zero — and report that zero as a confident "no" — is the most expensive bug you can ship. It doesn't just add noise; it removes the one mechanism that was carrying the loop, and it hides the removal.&lt;/p&gt;

&lt;p&gt;The design conclusion, stated as a single sentence: &lt;strong&gt;spend your inference budget on diverse generation plus stronger, independent, external verification — and build that verification so that when it fails, it says "I didn't check," never "the answer is no."&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Checklist: an honest verification gate
&lt;/h2&gt;

&lt;p&gt;Use this when you build or audit a self-checking loop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Three states, always.&lt;/strong&gt; Every verdict is &lt;code&gt;CONFIRMED&lt;/code&gt;, &lt;code&gt;REFUTED&lt;/code&gt;, or &lt;code&gt;ABSTAINED&lt;/code&gt;. There is no fourth bucket, and &lt;code&gt;ABSTAINED&lt;/code&gt; is never quietly merged into either of the others.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Classify on &lt;em&gt;valid&lt;/em&gt; votes first.&lt;/strong&gt; Count votes that actually ran and parsed &lt;em&gt;before&lt;/em&gt; you compare refutations. Below quorum of valid votes ⇒ &lt;code&gt;ABSTAINED&lt;/code&gt;, and you exit before the refute branch.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Errors/timeouts/nulls are abstentions.&lt;/strong&gt; A verifier that throws, times out, or returns nothing is &lt;code&gt;PENDING&lt;/code&gt; — retry up to &lt;em&gt;N&lt;/em&gt; with backoff. It is &lt;strong&gt;never&lt;/strong&gt; counted as clean and &lt;strong&gt;never&lt;/strong&gt; counted as refuted.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;A negative verdict must show evidence.&lt;/strong&gt; A real &lt;code&gt;REFUTED&lt;/code&gt; cites &lt;em&gt;why&lt;/em&gt; (a contradicting source, a failing check). If a "no" has no attached evidence, treat it as an abstention until proven otherwise.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Guard the summary layer.&lt;/strong&gt; Before any "inconclusive"/"all refuted" summary ships, inspect the vote shape. Abstention-dominated ⇒ report "verification did not complete," not "the claims are false."&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Keep a report-only reclassifier.&lt;/strong&gt; A tiny tool that re-reads a saved run's tallies and splits &lt;em&gt;refuted&lt;/em&gt; from &lt;em&gt;unverified&lt;/em&gt; turns a silent false-negative into a one-command catch.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Recover by resume, not restart.&lt;/strong&gt; Cache completed stages and successful votes; re-run only the abstained calls. Top up the tail sequentially and gently so you don't re-trip the limiter.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Watch for correlated failure.&lt;/strong&gt; If a whole batch "fails" at once, suspect shared infrastructure (rate limit, outage) before you believe a mass rejection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The discipline behind every line is one sentence worth memorizing: &lt;strong&gt;a verifier's silence is missing data, not a "no."&lt;/strong&gt; Build your loop so it can tell the difference, and it will fail honestly — which is the only kind of failure you can recover from.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Further reading:&lt;/strong&gt; the systematic treatment of test-time scaling for &lt;em&gt;agents&lt;/em&gt; (arXiv:2506.12928) and the 2026 benchmark of general agents that finds brute test-time scaling underdelivers — naming the context ceiling and the verification gap (arXiv:2602.18998); Multi-Agent Verification / Aspect Verifiers (arXiv:2502.20379) and rubric-guided test-time verification (arXiv:2601.15808) for verification as its own scaling axis; &lt;em&gt;Self-Correction Bench&lt;/em&gt; for the 64.5% blind spot and the "Wait" activation (arXiv:2507.02778); SETS for self-verify-and-correct loops (arXiv:2501.19306); and the reasoning-frontiers survey for the Reasoner/Verifier/Refiner vocabulary (arXiv:2504.09037).&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.danmercede.com/guides/verifier-abstention-not-refutation" rel="noopener noreferrer"&gt;danmercede.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>llm</category>
      <category>ai</category>
      <category>llmops</category>
    </item>
    <item>
      <title>Open-Source the Gate, Not the Verdict</title>
      <dc:creator>Dan Mercede</dc:creator>
      <pubDate>Fri, 26 Jun 2026 20:08:19 +0000</pubDate>
      <link>https://dev.to/danmercede/open-source-the-gate-not-the-verdict-4oo6</link>
      <guid>https://dev.to/danmercede/open-source-the-gate-not-the-verdict-4oo6</guid>
      <description>&lt;p&gt;It is tempting to think the intelligence is the product — that a sharper reviewer model is what makes AI code review safe. It is the opposite. The model that reads a diff is replaced within months. The thing that governs is the gate around it: the deterministic logic that default-denies on an unparseable or contradictory verdict and leaves a receipt of what it decided.&lt;/p&gt;

&lt;p&gt;So the gate is what you open-source and the gate is what does the governing. The model sits behind a command seam and is swapped without touching the enforcement around it. Publishing the verdict ties governance to a model version; publishing the gate gives you something that holds as the models churn underneath it. The durable artifact is the boundary, not the brain.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.danmercede.com/thoughts/2026-06-09-open-source-the-gate-not-the-verdict" rel="noopener noreferrer"&gt;danmercede.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>opensource</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
