<?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: Joseph Yeo</title>
    <description>The latest articles on DEV Community by Joseph Yeo (@josephyeo).</description>
    <link>https://dev.to/josephyeo</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%2F3863060%2F16d2095d-f941-4c53-822c-0e24424f303f.png</url>
      <title>DEV Community: Joseph Yeo</title>
      <link>https://dev.to/josephyeo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/josephyeo"/>
    <language>en</language>
    <item>
      <title>The Human in the Loop Doesn't Scale. I Kept Him Anyway.</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Sun, 28 Jun 2026 14:51:25 +0000</pubDate>
      <link>https://dev.to/josephyeo/the-human-in-the-loop-doesnt-scale-i-kept-him-anyway-2hi2</link>
      <guid>https://dev.to/josephyeo/the-human-in-the-loop-doesnt-scale-i-kept-him-anyway-2hi2</guid>
      <description>&lt;h2&gt;
  
  
  What it costs to be the last reviewer your own system has
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Part of the ForgeFlow series — building a coding agent that runs its execution loop locally on an M5 Max, and writing down what actually breaks. Planning runs on a frontier model; code generation runs on a local model via Ollama, test-driven inside a Docker sandbox.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the last post, I described how my agent's rulebook learned to forget — rules that age out get flagged, and a human decides whether to retire them. I ended on a line that's been nagging me ever since: the whole thing works &lt;em&gt;because I'm still small enough to read everything,&lt;/em&gt; and I suspect that doesn't last.&lt;/p&gt;

&lt;p&gt;This post is about that suspicion. It's about the design decision I keep reaching for — &lt;em&gt;keep a human in the loop&lt;/em&gt; — and the uncomfortable thing underneath it: that human is me, I am exactly one person, and "put a human on it" is not a plan. It's a debt I haven't been billed for yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  "Keep a human in the loop" is the answer I keep reaching for
&lt;/h2&gt;

&lt;p&gt;When an automated decision feels risky, my reflex is usually the same: don't let the machine do it alone, put a person on the final call. It sounds responsible. It's the answer I reach for when I don't yet trust the automation boundary, and it's the one I'd give if you asked me how to make an agent safe.&lt;/p&gt;

&lt;p&gt;And it's right, as far as it goes. A human on the final decision catches the failure modes a confidence score can't see. I argued for exactly this last time: the machine is good at noticing a rule hasn't earned its keep lately; it is not good at knowing whether that's because the rule is obsolete or because I just haven't exercised it. So the machine flags, and I decide.&lt;/p&gt;

&lt;p&gt;The part I glossed over is what "I decide" actually costs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The cost nobody puts on the diagram
&lt;/h2&gt;

&lt;p&gt;When you draw a human-in-the-loop system, the human is a small box near the end with an arrow labeled &lt;em&gt;approve / reject.&lt;/em&gt; The box looks free. It isn't. Every decision routed to that box spends three things that don't show up in the diagram.&lt;/p&gt;

&lt;p&gt;It spends &lt;strong&gt;attention&lt;/strong&gt; — each call needs enough context loaded into a human head to judge it, and that context isn't cached between decisions the way it is for a machine.&lt;/p&gt;

&lt;p&gt;It spends &lt;strong&gt;latency&lt;/strong&gt; — the system now moves at the speed of when I happen to look, not the speed of the runs. A flag raised at 2am waits for me. The agent doesn't.&lt;/p&gt;

&lt;p&gt;It spends &lt;strong&gt;a budget that doesn't grow&lt;/strong&gt; — the machine's side scales with hardware. My side barely scales at all. I get the same hours next year. If the flags-per-week curve goes up and the human-hours curve is flat, the lines cross. After they cross, "a human reviews it" quietly becomes "a human is supposed to review it," which is a different claim wearing the same label.&lt;/p&gt;

&lt;p&gt;That last failure is the one I actually fear. Not the human-in-the-loop that says no. The human-in-the-loop that has too much queued to look properly, and starts rubber-stamping — approving on a glance because the backlog is the real pressure. A reviewer who can't keep up doesn't fail loudly. They fail by &lt;em&gt;agreeing,&lt;/em&gt; and a rubber-stamped approval still moves the system forward — it just carries a decision nobody actually made, buried until something downstream breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I tried: making the human's time the scarce resource it actually is
&lt;/h2&gt;

&lt;p&gt;Once I stopped treating my own attention as free, the design question flipped. It stopped being "where should a human review?" and became "this human has a small, fixed number of real decisions in him per week — which ones are worth spending?"&lt;/p&gt;

&lt;p&gt;That reframing changed the design more than another automation pass would have. A few things fell out of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most decisions don't need me; they need a default.&lt;/strong&gt; A flag I almost always approve isn't a decision, it's a ceremony. The honest move is to pick the safe default, let it happen automatically, and log it where I can audit a sample later — not to stand at the gate nodding. The clearest case: stale-rule flags for rules that only ever applied to throwaway scaffolding from old projects. There's no real call to make there — retiring them is the safe default, so the system does it and tells me, instead of asking. I moved a whole class of "review" into "do the safe thing and log it," and got the time back for the calls that were actually close.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The decisions worth my time are the irreversible and the ambiguous.&lt;/strong&gt; Retiring a rule that can't easily be un-retired; anything where the machine's confidence is truly split rather than just low. Those I keep. They're rare, which is the point — keeping a human in the loop only scales if the loop is &lt;em&gt;small.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batching beats interrupting.&lt;/strong&gt; Ten flags reviewed in one sitting, with shared context, cost a fraction of ten flags reviewed across ten interruptions. So the system holds non-urgent decisions and presents them together, instead of paging me the moment each one appears.&lt;/p&gt;

&lt;p&gt;None of this removes the human. It does the opposite — it admits the human is the bottleneck and budgets around it, instead of pretending the bottleneck is free and acting surprised when it backs up.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I deliberately refused to automate, even knowing it doesn't scale
&lt;/h2&gt;

&lt;p&gt;Here's the tension I haven't resolved.&lt;/p&gt;

&lt;p&gt;Some decisions I keep for myself &lt;em&gt;even though&lt;/em&gt; I know that choice is what limits how far the system can run without me. Retiring hard-won knowledge is one. The call to let the agent act on something it's never done before is another. Not because a model couldn't make those calls — increasingly it could — but because I'm not yet willing to not know when they happen.&lt;/p&gt;

&lt;p&gt;That's an honest admission, not a principle. It might be that I'm holding onto these out of caution that's already obsolete. It might be that one of them is the next thing to hand off, and I'm just attached. The reason I can still tell the difference is, again, that I'm small enough to feel each of these decisions individually. The day there are too many to feel is the day this stops being judgment and starts being a story I tell myself about judgment.&lt;/p&gt;

&lt;p&gt;So I'm not claiming I solved it. I'm claiming I stopped pretending the human was free, and that alone changed which decisions I let reach me.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this didn't prove
&lt;/h2&gt;

&lt;p&gt;This is one person's setup, and the bottleneck I'm describing is &lt;em&gt;me,&lt;/em&gt; specifically — my hours, my attention, my unwillingness to look away from certain decisions. A team has a different shape of this problem: more reviewers, but also coordination cost, and the rubber-stamp failure mode gets easier to hide, not harder, when "someone reviewed it" can mean anyone.&lt;/p&gt;

&lt;p&gt;I haven't shown that my particular triage — defaults for the routine, human for the irreversible and the ambiguous — is the right cut. It's the cut that fit a single-operator system small enough to audit by sampling. I also haven't escaped the core problem; I've only delayed it. Every decision I automate to save my attention is a decision I now have to trust without watching, which is the exact move the earlier posts in this series were nervous about. I traded one risk for another with my eyes open. That's not a solution. It's a position.&lt;/p&gt;




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

&lt;p&gt;"Keep a human in the loop" is true and incomplete. It's true because the human catches what the metric can't. It's incomplete because it quietly assumes the human's time is free, and in my setup the human's time is the least scalable resource in the whole system. A loop with a person in it only works while the loop stays small enough that the person can actually be in it — not nominally, actually.&lt;/p&gt;

&lt;p&gt;If I had to compress it: the goal isn't to keep a human in the loop. It's to spend the human on the decisions that deserve a human, and to be honest that everything else was a default you chose, not a review you did.&lt;/p&gt;




&lt;p&gt;I'd like to hear how others handle this, because I don't think being small saves me for long. In your systems, what do you actually keep a person on — and how do you tell the difference between a review that's real and one that's become a rubber stamp? And when you handed a decision to automation, how did you decide it was safe to stop watching?&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>llm</category>
    </item>
    <item>
      <title>What I Learned by Deleting Rules My Agent Had Already Learned</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Fri, 26 Jun 2026 16:03:00 +0000</pubDate>
      <link>https://dev.to/josephyeo/what-i-learned-by-deleting-rules-my-agent-had-already-learned-2cak</link>
      <guid>https://dev.to/josephyeo/what-i-learned-by-deleting-rules-my-agent-had-already-learned-2cak</guid>
      <description>&lt;h2&gt;
  
  
  Knowledge that isn't pruned starts to mislead you
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Part of the ForgeFlow series — building a coding agent that runs its execution loop locally on an M5 Max, and writing down what actually breaks. Planning runs through a separate planning step; code generation runs on a local model via Ollama, test-driven inside a Docker sandbox.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;For months, my agent got better by accumulating rules.&lt;/p&gt;

&lt;p&gt;Every time a project failed in a way I understood, I'd write the lesson down as a rule and feed it back in — &lt;em&gt;don't do this, always do that&lt;/em&gt; — so the agent wouldn't repeat the mistake. To be clear about what "rules" means here: not fine-tuning, not weights. A plain, human-readable rulebook the system injects into the agent's context, built up from past failures. For a long time, adding to it worked. Autonomy went up. The same mistakes stopped coming back.&lt;/p&gt;

&lt;p&gt;Then the agent started getting &lt;em&gt;worse&lt;/em&gt;, and the rulebook was the reason.&lt;/p&gt;

&lt;p&gt;Not because the rules were wrong. Because some of them had &lt;em&gt;stopped&lt;/em&gt; being right — and I'd never built anything to notice. This post is about the part of the system I hadn't built: the part that lets old knowledge leave.&lt;/p&gt;

&lt;p&gt;(This continues a thread from a recent run of posts about verifying your own system. The short version of that run: every layer you trust should be measured before you trust it. This is what happens when you forget that even earned trust has an expiry date.)&lt;/p&gt;




&lt;h2&gt;
  
  
  The half I built first: a place to write lessons down
&lt;/h2&gt;

&lt;p&gt;The first half of this is the obvious, satisfying half. Your agent fails, you diagnose it, you encode the fix as a rule, and the failure doesn't recur. Do that across a few dozen projects and you've got a real rulebook — concrete, earned, specific. Mine grew to around 77 rules at one point, and I wrote a whole post about how good that felt ("77 Rules Later"). Each rule had a story behind it. Each one had prevented something real.&lt;/p&gt;

&lt;p&gt;This is the part that feels like progress, because it &lt;em&gt;is&lt;/em&gt; progress — at first. A rulebook induced from actual failures is far better than generic advice like "write clean code." It captures things like a specific framework-level behavior that only surfaces when two configuration paths interact — the kind of thing that turns a multi-hour debugging session into a one-line guardrail.&lt;/p&gt;

&lt;p&gt;So you keep adding. Why wouldn't you? Every rule paid for itself once.&lt;/p&gt;

&lt;p&gt;The hidden assumption in "why wouldn't you" is that a rule, once true, stays true. That's the assumption that broke.&lt;/p&gt;




&lt;h2&gt;
  
  
  The half I had missed: a place for lessons to expire
&lt;/h2&gt;

&lt;p&gt;Somewhere past a few dozen rules, I noticed the curve bend the wrong way. More rules stopped producing better behavior. Past a point, they started producing &lt;em&gt;slightly worse&lt;/em&gt; behavior — more hesitation, more rules half-applied, the occasional case where two rules pulled in different directions and the agent picked the wrong one to honor.&lt;/p&gt;

&lt;p&gt;This surprised me, because my default instinct with the agent was to give it more context. More rules, more examples, more guardrails — surely more is safer. But a rulebook isn't free. Every rule you inject is something the model has to hold, weigh, and reason around on every step. Past some threshold, the cost of carrying a rule can exceed the failure it still prevents — especially when the thing it was written to prevent no longer happens.&lt;/p&gt;

&lt;p&gt;And that's the category I'd completely missed: rules that had simply &lt;em&gt;aged out.&lt;/em&gt; A rule written for one version of a library, after the library changed. A guardrail for a failure mode I'd since fixed at the source, so it could never recur — but the rule kept riding along, costing attention, preventing nothing. I had a careful process for &lt;em&gt;adding&lt;/em&gt; knowledge and no process at all for &lt;em&gt;removing&lt;/em&gt; it. The rulebook could only grow.&lt;/p&gt;

&lt;p&gt;A rulebook that can only grow stops being a curated knowledge base. It quietly turns into an index where the dead entries dilute the living ones — and nothing in it tells you which is which.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a stale rule is worse than no rule
&lt;/h2&gt;

&lt;p&gt;Here's the part worth sitting with, and it rhymes with something I keep running into in this project: the failures that cost me most were the quiet ones.&lt;/p&gt;

&lt;p&gt;A &lt;em&gt;missing&lt;/em&gt; rule is loud. The agent makes a mistake, you notice, you add the rule. The system has a built-in way to surface what it doesn't know — failure.&lt;/p&gt;

&lt;p&gt;A &lt;em&gt;stale&lt;/em&gt; rule is silent. It was right when you wrote it. It reads as authoritative because it earned that authority, once. It sits in the rulebook looking exactly like the rules that are still true, and nothing about it announces that the world it described has moved on. The agent dutifully follows it. You don't get a failure that points at it — you get slightly degraded behavior with no obvious cause, spread thin across everything the agent does.&lt;/p&gt;

&lt;p&gt;That's why a stale rule can be worse than no rule. A missing rule often leaves a gap you can see once the failure appears. A stale rule fills the gap with confident, outdated instruction, and confident-and-outdated is one of the harder kinds of wrong to catch — because everything looks fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I built instead: rules with a confidence score and a way to die
&lt;/h2&gt;

&lt;p&gt;The fix wasn't cleverer rules. It was treating each rule as something other than a permanent truth.&lt;/p&gt;

&lt;p&gt;The reframing that helped: a rule is not a fact. It's a &lt;em&gt;hypothesis with an evidence record.&lt;/em&gt; It claims "doing X prevents failure Y," and that claim is either being supported by what actually happens in real runs, or it isn't. So instead of a flat list, each rule now carries a small amount of state: how confident I currently am in it, and what recent evidence supports or undercuts it.&lt;/p&gt;

&lt;p&gt;The lifecycle, in plain terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A new rule starts as a &lt;strong&gt;candidate&lt;/strong&gt; — written down, but not yet trusted. It hasn't earned its place.&lt;/li&gt;
&lt;li&gt;If real runs keep showing evidence that the rule is still guarding against an active failure mode, confidence rises and the rule becomes &lt;strong&gt;trusted.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;If a rule stops getting that supporting evidence — the failure it guards against simply isn't occurring anymore, across many runs — its confidence decays. Below a threshold, it's flagged &lt;strong&gt;stale&lt;/strong&gt; for review.&lt;/li&gt;
&lt;li&gt;A stale rule that, on inspection, appears to guard against something that can no longer happen gets &lt;strong&gt;retired.&lt;/strong&gt; Out of the rulebook, into an archive, where it's recorded but no longer injected.
One honest caveat about that evidence: I can't prove a counterfactual. I don't &lt;em&gt;know&lt;/em&gt; the failure would have happened without the rule — I'm inferring it from run traces and the shape of what the agent attempted, not from a controlled A/B where I remove the rule and watch the system break. The confidence score reflects that inference, not a proof. I keep that in mind whenever I read it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm deliberately not giving the exact scoring here, partly because the specific numbers are tuned to my system and would be noise to you, and partly because the point isn't the formula. The point is the &lt;em&gt;direction&lt;/em&gt;: confidence flows from evidence, evidence comes from real runs, and a rule with no recent supporting evidence is a liability until proven otherwise — not an asset by default. Adding a rule is cheap. Letting it persist without review is where the cost accumulates.&lt;/p&gt;

&lt;p&gt;If this sounds like cache invalidation, or like deprecating a feature flag, or like paying down tech debt — yes. It's the same shape: the cost isn't in creating the thing, it's in the discipline of removing it when it stops earning its keep. I just hadn't been treating &lt;em&gt;lessons&lt;/em&gt; as something that needed garbage collection.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part I deliberately didn't automate
&lt;/h2&gt;

&lt;p&gt;The obvious next step would be to close the loop completely: let the system retire its own rules the moment confidence drops. I didn't, and I think the reason matters.&lt;/p&gt;

&lt;p&gt;A confidence score is itself a measurement, and measurements can be wrong. A rule might look unsupported simply because I haven't run the kind of project that triggers its failure mode lately — not because it's obsolete. If I let the system auto-delete on a low score, it could kill a good rule during a quiet stretch, and I'd only find out when the old failure came roaring back.&lt;/p&gt;

&lt;p&gt;So the system doesn't delete. It &lt;em&gt;proposes.&lt;/em&gt; A rule going stale raises a flag for me to look at, and retiring it is a decision I still make. The machine is good at noticing "this rule hasn't earned its keep lately." It is not good at knowing whether that's because the rule is obsolete or because I just haven't stress-tested it. That difference needs a human, at least for now.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this didn't prove
&lt;/h2&gt;

&lt;p&gt;I'll keep this honest, the way I've tried to with the rest of these.&lt;/p&gt;

&lt;p&gt;This is one system's experience — a single-person setup with a rulebook small enough that I can still read all of it. I haven't shown that knowledge decay matters at every scale, or that my particular lifecycle is the right one. It's an approach that worked in this setup, not a general result. For a small or short-lived project, the whole apparatus is overkill; a rulebook you fully re-read every week doesn't need confidence scores, it needs you to read it.&lt;/p&gt;

&lt;p&gt;And the hard part isn't solved. "Has this rule earned its keep lately?" is a real signal, but it's a &lt;em&gt;proxy&lt;/em&gt;, and proxies drift too — the watcher needs watching, same as everything else in this series. I don't have a clean rule for when a quiet rule is obsolete versus merely untested, which is exactly why I kept a human in that decision. I'm sharing the shape because it changed how I think about my own knowledge base, not because it's finished.&lt;/p&gt;




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

&lt;p&gt;In my own system, I spent enormous effort teaching it new things and almost none deciding when old things should stop being believed. A knowledge base that can only grow may stop getting wiser; past a point, it can get slower and quietly less correct, because the rules that have gone stale look exactly like the rules that are still true.&lt;/p&gt;

&lt;p&gt;If I had to compress it: writing a lesson down is the easy half. Letting it expire when it no longer earns its place is the half that keeps the lessons honest.&lt;/p&gt;




&lt;p&gt;I'd like to know how other people handle this. In your systems — prompt libraries, rule sets, runbooks, internal docs, lint configs — does anything ever &lt;em&gt;retire&lt;/em&gt;, or does it all just accumulate? And if things do get removed, how do you decide a piece of hard-won knowledge has stopped being true? I worked out a rough answer for my case, but it leans on being small enough to still read everything, and I suspect that doesn't last.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>coding</category>
      <category>llm</category>
    </item>
    <item>
      <title>We Stopped Trusting Models. Then We Stopped Trusting Our Own Numbers.</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Tue, 23 Jun 2026 16:31:06 +0000</pubDate>
      <link>https://dev.to/josephyeo/we-stopped-trusting-models-then-we-stopped-trusting-our-own-numbers-1611</link>
      <guid>https://dev.to/josephyeo/we-stopped-trusting-models-then-we-stopped-trusting-our-own-numbers-1611</guid>
      <description>&lt;h2&gt;
  
  
  Nondeterminism isn't a bug to ban — it's a force to place
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Part of the ForgeFlow series — building a coding agent that runs its execution loop locally on an M5 Max, and writing down what actually breaks. Planning runs on Claude; code generation runs on a local model via Ollama, test-driven inside a Docker sandbox.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;A while back I wrote that we'd stopped chasing better models — that for this project, swapping in a stronger model kept failing to fix problems that turned out to be about the system around the model, not the model itself. That post ended on a tidy note: the model wasn't the bottleneck, the system was.&lt;/p&gt;

&lt;p&gt;This is the post where the same suspicion turns inward, toward our own measurements. Because once you stop trusting the model to be the answer, the next thing you lean on is your own &lt;em&gt;measurements&lt;/em&gt; — the test counts, the gate statistics, the tallies that tell you whether the system is working. And over a stretch of building, I learned those can't be trusted blindly either. The three previous posts in this run were each a version of that discovery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A test suite that &lt;strong&gt;passed&lt;/strong&gt; while measuring the wrong environment.&lt;/li&gt;
&lt;li&gt;A gate that &lt;strong&gt;blocked&lt;/strong&gt; 198 times while being wrong often enough that the count could no longer serve as evidence of quality.&lt;/li&gt;
&lt;li&gt;An agent that &lt;strong&gt;counted&lt;/strong&gt; twelve when the real number was thirteen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three different instruments. A recurring failure shape underneath them: &lt;em&gt;the thing I use to verify can itself be wrong, and it tends to be wrong in a way that looks like success.&lt;/em&gt; This post is about what I took from that — and, more importantly, about the wrong conclusion I almost drew from it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The wrong lesson: "ban the uncertainty"
&lt;/h2&gt;

&lt;p&gt;When you get burned three times by measurements you trusted, there's an instinctive reaction: trust nothing that isn't certain. Push every source of uncertainty out of the system. Treat nondeterminism — anything probabilistic, anything that might come out differently twice — as a defect to eliminate. If the language model is the uncertain part, minimize the language model. Make everything deterministic and tell yourself you can finally sleep soundly.&lt;/p&gt;

&lt;p&gt;I leaned that way for a while. It's wrong, or at least too blunt to be useful. Taken seriously, "ban all nondeterminism" throws out the single most valuable thing the model brings to this system — its ability to &lt;em&gt;propose&lt;/em&gt;, to &lt;em&gt;explore&lt;/em&gt;, to suggest a fix I wouldn't have enumerated. You can't get that from a deterministic rule. The uncertainty isn't only a liability; in the right seat, it's the entire point.&lt;/p&gt;

&lt;p&gt;So the question stopped being "how do I remove the uncertainty" and became "&lt;strong&gt;where does the uncertainty belong?&lt;/strong&gt;"&lt;/p&gt;




&lt;h2&gt;
  
  
  The better lesson: place it, don't ban it
&lt;/h2&gt;

&lt;p&gt;Here's the reframing that held up — and it took an embarrassingly long time to see, partly because I'd quietly assumed nondeterminism was a debt the whole time without ever checking the assumption.&lt;/p&gt;

&lt;p&gt;A system like this has two kinds of seats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seats that propose and explore.&lt;/strong&gt; What should we try next? What might this failure be? What's a candidate fix? These seats &lt;em&gt;want&lt;/em&gt; nondeterminism. A probabilistic model generating possibilities is a feature here, not a risk. If it's occasionally wrong, the cost is low — being wrong is fine when you're only suggesting, because something downstream still has to approve you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seats that judge and record.&lt;/strong&gt; Did the tests actually pass? Is this allowed through? What gets written down as true? These seats can't tolerate an unaccountable input. They need to be deterministic, reproducible, and checkable — because this is exactly where "almost right" becomes indistinguishable from "right," which is the trap the last three posts kept falling into.&lt;/p&gt;

&lt;p&gt;Each of the three failures was the same shape: something that shouldn't have been the final authority had crept into a &lt;em&gt;judging&lt;/em&gt; seat. The polluted environment let an unpinned variable decide a verdict. The miscalibrated gate let an unchecked heuristic sit in judgment and reject valid work. The agent's self-report nearly let a probabilistic counter be the last word on a number. In every case, the fix wasn't to purge uncertainty from the whole system — it was to get the wrong thing &lt;em&gt;out of the judging seat&lt;/em&gt;, and make sure that seat was held by something deterministic, pinned, and witnessable.&lt;/p&gt;

&lt;p&gt;Nondeterminism, it turns out, isn't a quality of the system to be turned up or down. It's a &lt;em&gt;force to be placed.&lt;/em&gt; You let it run at the front, where things are proposed and explored. You keep it out of the back, where things are judged and recorded. The model proposes; a deterministic check decides. That one division became the thread that connected the fixes in this run.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this isn't just "trust deterministic things more"
&lt;/h2&gt;

&lt;p&gt;It's tempting to read all this as "deterministic good, probabilistic bad." That's not it, and the distinction matters.&lt;/p&gt;

&lt;p&gt;A probabilistic suggestion in a &lt;em&gt;proposing&lt;/em&gt; seat is more valuable than a deterministic one, because it can reach things a rule can't. And a deterministic check is only as good as whether it's actually correct and checkable — the gate in the second post was deterministic and still wrong a lot of the time, and a deterministic judge that's consistently wrong can be worse than a coin flip, because it's a &lt;em&gt;steady&lt;/em&gt; error you eventually stop questioning. So it isn't that determinism is virtuous and uncertainty is sinful. It's that they belong in different places, and the whole engineering problem is keeping them sorted:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Let the uncertain thing explore. Let deterministic checks judge — but only after the checks themselves have been checked.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reason this took four posts and a fair amount of getting it wrong is that the failures don't announce which category they're in. A measurement that's quietly wrong looks exactly like one that's right, until you go and look. The sorting isn't automatic. You do it deliberately, instrument by instrument, and you usually find out you got it wrong only when a number you trusted turns out to have been the problem all along.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this didn't prove
&lt;/h2&gt;

&lt;p&gt;I want to close the series the way I've tried to write the whole thing — without inflating it.&lt;/p&gt;

&lt;p&gt;This is a framing that helped &lt;em&gt;one&lt;/em&gt; system: a local AI coding agent with a test-driven loop, where I happen to have the luxury of pinning judging seats to deterministic checks I can watch. I haven't proven it's the right decomposition for every AI system, and I'd be wary of anyone — including me — who turned "place nondeterminism, don't ban it" into a universal law. It's a working lens, not an established result. The evidence behind it is a handful of incidents, honestly reported, not a controlled study.&lt;/p&gt;

&lt;p&gt;It also doesn't resolve the hard part: the boundary between "propose" and "judge" is not always obvious. Plenty of real decisions are a blend — a judgment that also has to weigh uncertain evidence, or a proposal that quietly commits you to something before anything else gets a vote. Where exactly to put the deterministic check, and how much to let the probabilistic part inform a judgment without &lt;em&gt;becoming&lt;/em&gt; the judgment, is something I'm still working out. The previous post's open question — when a human-witnessed check can graduate to a trusted automated one — is part of the same unsolved area. I'm sharing the lens because it organized a mess for me, not because it's finished.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway, for the whole run
&lt;/h2&gt;

&lt;p&gt;Four posts, one thread. We stopped trusting that a better model would save us. Then we learned not to trust our own measurements blindly either — not the passing tests, not the busy gate, not the agent's tidy count. What survived all that doubt, for me, was a single discipline that proved sturdy enough to stand on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In systems like this, every layer that grants trust should be measured before it's trusted — and that measurement should be grounded, wherever possible, in something deterministic you can witness.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's it. Not a model, not a framework, not a clever architecture. A place to stand. After doubting the model, the gate, the tally, and our own numbers, it's the one thing I found solid enough to build the next thing on.&lt;/p&gt;




&lt;p&gt;This is the end of this short run, so I'll ask the broad version of the question. For those building systems with AI in the loop: where do you draw the line between the parts you let be uncertain and the parts you insist be deterministic — and how do you check that you drew it in the right place? I arrived at one answer through a series of mistakes. I'd rather learn the next one from other people's experience than from my own next mistake.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for following this run. The earlier ForgeFlow posts — on the local agent itself, on why we stopped chasing models, and on what breaks at scale — are linked from my profile.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>softwareengineering</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>My Agent Reported 12. The Real Number Was 13.</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Mon, 22 Jun 2026 11:47:21 +0000</pubDate>
      <link>https://dev.to/josephyeo/my-agent-reported-12-the-real-number-was-13-5864</link>
      <guid>https://dev.to/josephyeo/my-agent-reported-12-the-real-number-was-13-5864</guid>
      <description>&lt;h2&gt;
  
  
  Why you have to witness the measurement before you trust the instrument
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Part of the ForgeFlow series — building a coding agent that runs its execution loop locally on an M5 Max, and writing down what actually breaks. Planning runs on Claude; code generation runs on a local model via Ollama, test-driven inside a Docker sandbox.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I was building the part of the system that measures itself, and I'd reached the point where it felt safe to let the AI agent do the counting. The task was mundane: tally how often a certain check had fired, aggregate it, report a number. The agent ran and reported 12.&lt;/p&gt;

&lt;p&gt;I almost took it. It was a believable number, produced confidently, and I was tired of doing this kind of bookkeeping by hand. But something made me run it down myself in the terminal. The real count was 13. One item that belonged in the tally had been silently dropped from the agent's raw count.&lt;/p&gt;

&lt;p&gt;One off. Twelve versus thirteen. On its own, trivial. But it landed on a question I'd been circling for a while — &lt;em&gt;who is allowed to be the final witness to a measurement?&lt;/em&gt; — and the answer changed how I let AI into this system. This is the third post in a short run about that larger theme: the tools I use to verify my work can themselves be wrong, usually in ways that look fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I wanted to hand off the counting
&lt;/h2&gt;

&lt;p&gt;The honest reason was fatigue. A self-measuring system generates a lot of small, exact bookkeeping — count the occurrences, filter the relevant ones, divide, summarize. It's beneath an agent's apparent ability and above my patience, so handing it off felt obvious. The agent reads the data, the agent reports the number, I read the report. Clean.&lt;/p&gt;

&lt;p&gt;And to be fair to the agent: most of what it did was right. It wasn't hallucinating wildly. It produced a number that was &lt;em&gt;almost&lt;/em&gt; correct — off by one, in a way that would have been invisible if I hadn't gone and looked. That's the dangerous kind of wrong: not obviously broken, just plausible enough to accept.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one that got away
&lt;/h2&gt;

&lt;p&gt;Here's how I know the real number was 13 and not 12, because "the agent was wrong" needs a ground truth. I didn't ask the agent to summarize. I ran a deterministic pass in the terminal that emitted each matching record, one by one, from the same source data, under the same inclusion rule — and then counted those emitted records directly. Run it again, same 13. The missing entry met that same rule; it just arrived in an irregular shape, unlike the other twelve, so the agent's summarizing pass had skipped it. Thirteen by reproducible enumeration, twelve by agent summary. The difference wasn't a matter of my opinion — it was the same data, counted two ways, where only one of the two ways could be re-run to the same answer.&lt;/p&gt;

&lt;p&gt;Now the detail that actually unsettled me. The &lt;em&gt;final&lt;/em&gt; number I cared about — a summarized metric, produced after a downstream step reduced the raw count to a coarser, normalized figure — came out the same whether the raw count was 12 or 13. It was the kind of step (rounding, bucketing) where a difference of one can simply vanish. So if I'd checked only the headline metric, I'd have seen the agent's report agree with mine and concluded everything was fine. Only the &lt;em&gt;raw&lt;/em&gt; count underneath disagreed.&lt;/p&gt;

&lt;p&gt;That's the part worth sitting with — and it's where "the final number was the same, so what's the problem?" gets answered. The problem was &lt;em&gt;not&lt;/em&gt; that this particular headline metric changed. It didn't. The problem was that the raw measurement layer was already wrong, and the agreement at the normalized layer would have hidden that fact. A different threshold, a different aggregation, or the next consumer downstream might not have absorbed the same error — and once the raw layer is wrong, every later use of it (an audit, a trend line, a regression check) inherits the error. The safety here wasn't something I'd designed. It was luck, and luck doesn't generalize.&lt;/p&gt;

&lt;p&gt;So the lesson wasn't "the agent made an arithmetic mistake." Agents make mistakes; that's expected. The lesson was about &lt;em&gt;who I'd let stand as the last check.&lt;/em&gt; I'd been about to let the agent's self-report be the final word on a measurement, and the self-report was wrong in a way only a deterministic, re-runnable count would catch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The instrument can't be the only witness to itself
&lt;/h2&gt;

&lt;p&gt;Let me state the principle the way it finally settled, because it's the part I'd defend.&lt;/p&gt;

&lt;p&gt;When you build a measuring system, there's a pull to let the same system that &lt;em&gt;does&lt;/em&gt; the work also &lt;em&gt;judge&lt;/em&gt; whether it came out right — especially when that system is a capable model that can read logs, count, and summarize. It's convenient. But it quietly inverts the roles. The model's judgment is supposed to be &lt;em&gt;assistance&lt;/em&gt;. If it becomes the only thing standing between you and the recorded truth, then the model has become the source of truth — and a probabilistic, occasionally-off-by-one source of truth is sitting in the one seat that should be reproducible and auditable.&lt;/p&gt;

&lt;p&gt;The fix isn't to stop using the agent. It's to insist that, at least while I'm still establishing trust, a &lt;em&gt;human deterministically witnesses the output&lt;/em&gt; — that I sit at the terminal and watch the real number come out, with my own eyes, before I let the automated measurement stand on its own. The model can do the heavy lifting. It just can't, yet, be the sole witness to whether the heavy lifting was correct — because verifying a measurement is exactly where "almost right" is indistinguishable from "right" until something deterministic says otherwise.&lt;/p&gt;

&lt;p&gt;It's a little uncomfortable to write down, because it means the convenient version of the workflow — agent counts, agent confirms, I read the summary — is the one I specifically can't have when the number matters.&lt;/p&gt;




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

&lt;p&gt;Two adjustments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A human witness before the automation is trusted.&lt;/strong&gt; Before I let any self-measurement run unattended, I make the deterministic version happen in front of me at least once — the actual count, from the actual data, watched as it's produced, and re-runnable to the same result. If the automated path and the watched path disagree, the automated path is wrong until proven otherwise. The agent's confidence carries no weight in that comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pin the measurement to units I can actually witness.&lt;/strong&gt; Part of why the slip nearly slid through was a mismatch between what the agent tallied and what was legitimately in scope. So I narrowed the measurement to count only the units that were genuinely instrumented and observable — the units a human can directly observe and enumerate — rather than a looser population the agent gets to interpret. When the population is well-defined, the hand-witnessed number and the machine number can actually be compared. When it's loose, they drift, and you can't even tell.&lt;/p&gt;

&lt;p&gt;Both of these slow things down. That's the cost I'm deliberately paying for the measurements I'm willing to bet on.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this didn't prove
&lt;/h2&gt;

&lt;p&gt;I want to keep this in proportion.&lt;/p&gt;

&lt;p&gt;This is one off-by-one, caught once, on one counting task. It doesn't prove AI agents can't count, or that they're untrustworthy in general, or that you should hand-verify every number an agent produces. For many low-stakes tallies, the agent's number may be good enough, and re-counting by hand would waste a perfectly good tool. I'm not arguing for paranoia.&lt;/p&gt;

&lt;p&gt;The claim I'd stand behind is narrow and conditional: &lt;em&gt;for measurements you intend to build further conclusions on, the final witness should be deterministic and, at least initially, human.&lt;/em&gt; The stakes set the bar. A throwaway internal count doesn't need this. A number you're going to let the system's self-assessment ride on does — because that's the number whose error compounds.&lt;/p&gt;

&lt;p&gt;I'd also flag the obvious tension: this doesn't scale by hand forever. The whole point of automating measurement was to &lt;em&gt;not&lt;/em&gt; sit and count things. So "a human witnesses it" is a phase, not a permanent state — it's how trust gets established before the automation runs on its own, not a vow to recount reality by hand every day. For now, I only start relaxing the human witness once the deterministic path and the automated path have matched across repeated runs, and once the inclusion rule is pinned tightly enough that both are counting the same population. Where exactly to graduate beyond that is something I'm still working out, and I don't have a clean rule for it yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway, stated honestly
&lt;/h2&gt;

&lt;p&gt;You can let an AI write the code. You can let it run the analysis. But if you also let the &lt;em&gt;same system that produced the answer&lt;/em&gt; be the final judge of whether that answer was correct, you've made the examinee the examiner, and the score gets hard to trust on its own. For the numbers that matter, something deterministic — and, while trust is still being earned, something human — has to be the last thing that looks.&lt;/p&gt;

&lt;p&gt;The agent was right about almost everything — off by one. The whole question is whether you find out about the one, and that depended entirely on whether I was willing to look myself.&lt;/p&gt;




&lt;p&gt;How do others draw this line? When you let an AI agent measure or aggregate something, do you verify it independently — and how do you decide which numbers earn the hand-check and which numbers you allow to pass without it? I leaned on doing it myself this time, but I know that doesn't scale, and I'd be glad to hear how people who've gone further handle the trust handoff.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next in the series — the last of this run: we stopped chasing better models a while ago. This is about the moment we stopped trusting our own measurements too, and what we kept instead.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>softwareengineering</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>The Gate Fired 198 Times. I Called It "Working."</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Sun, 21 Jun 2026 17:39:52 +0000</pubDate>
      <link>https://dev.to/josephyeo/the-gate-fired-198-times-i-called-it-working-45fk</link>
      <guid>https://dev.to/josephyeo/the-gate-fired-198-times-i-called-it-working-45fk</guid>
      <description>&lt;h2&gt;
  
  
  Why a blocked count is not a success metric
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Part of the ForgeFlow series — building a coding agent that runs its execution loop locally on an M5 Max, and writing down what actually breaks. Planning runs on Claude; code generation runs on a local model via Ollama, test-driven inside a Docker sandbox.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I built a gate to block bad code. It blocked 198 pieces of code, and I took that number as evidence the gate was working well.&lt;/p&gt;

&lt;p&gt;Then I opened the blocked cases and read them one by one, checking each against the acceptance criteria for the task it came from. A large share of them weren't bad code. The gate had been wrong often enough that I could no longer read the block count as evidence it was working — it had been firing constantly, exactly as it was designed to fire, and I'd mistaken "it fires a lot" for "it's doing its job." Those are not the same statement.&lt;/p&gt;

&lt;p&gt;This is the second post in a short run about something I kept tripping over while building this agent: the things I use to verify my system can themselves be broken, and they tend to break in ways that look like success. The &lt;a href="https://dev.to/josephyeo/when-pytest-said-passed-it-was-lying-43p7"&gt;last post&lt;/a&gt; was about a test run that lied by passing. This one is about a gate that lied by blocking.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the gate is, and why I trusted the count
&lt;/h2&gt;

&lt;p&gt;The agent works in a test-driven loop, and one step in that loop is a gate: before certain code is allowed through, the gate checks that it meets a standard, and if it doesn't, it blocks the code and sends the work back to be redone. The gate exists to keep weak or malformed attempts from getting committed.&lt;/p&gt;

&lt;p&gt;For a while, the gate's headline number was how many times it had blocked something: 198. I looked at that and felt good about it. The reasoning felt obvious: the gate is catching a lot of bad attempts, so the gate is valuable, so the system is healthier for having it. High block count, hard-working gate, fewer bad commits. Why look closer?&lt;/p&gt;

&lt;p&gt;That reasoning has a hole in it — and that hole is what this post is about.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two claims I'd quietly merged
&lt;/h2&gt;

&lt;p&gt;When I went through the blocked cases — not the count, the actual cases — I found that a large share of what the gate had blocked was work that was fine. Not malformed, not weak. Legitimate attempts that happened to take a shape the gate didn't recognize, so it rejected them.&lt;/p&gt;

&lt;p&gt;I want to be precise about what "fine" means here, because "the gate was wrong" needs a ground truth or it's just my opinion. By ground truth I don't mean "I liked the code." I mean each step had explicit acceptance criteria: the targeted tests it was meant to pass, the behavior it was meant to produce, and the stated constraints for that step. A block was a &lt;em&gt;false positive&lt;/em&gt; when those criteria were already satisfied and the gate rejected the work anyway, for a property that wasn't part of the task's success condition.&lt;/p&gt;

&lt;p&gt;For example: a solution that passed every test it was supposed to pass, but got rejected because its internal structure didn't match what the gate's check expected — the same task-level behavior, in a representation the task itself never required. That isn't a question of taste. The work met the criteria; the gate said no for a reason that sat outside them.&lt;/p&gt;

&lt;p&gt;So the 198 was real. Every one of those blocks happened. What was false was the meaning I'd attached to it. I had collapsed two different claims into one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The gate fired.&lt;/strong&gt; (True. 198 times. Verifiable.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The firing was justified.&lt;/strong&gt; (Never checked — and, it turned out, often not.)
"It blocked something" and "it was right to block that something" are independent facts. A gate can be extremely active and extremely wrong at the same time — and a &lt;em&gt;miscalibrated&lt;/em&gt; gate will tend to be both, because the same flaw that makes it reject good work also makes it reject a lot of it. The block count I'd treated as evidence of value is equally consistent with a gate that's simply trigger-happy. The block count alone can't separate a justified rejection from a false positive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This seems like an easy mistake to make when building guards — linter rules, CI checks, validation layers, policy filters. At least it was in my case, and I suspect it's more common than we'd like to admit. The dashboard shows you &lt;em&gt;activity&lt;/em&gt;. Activity feels like protection. But activity is not the same as &lt;em&gt;correct&lt;/em&gt; activity, and the dashboard usually doesn't know the difference, so it shows you the comforting number and lets you supply the flattering interpretation.&lt;/p&gt;




&lt;h2&gt;
  
  
  "Then how did anything get through?"
&lt;/h2&gt;

&lt;p&gt;That's the fair question, and it's the one that finally made me look. If the gate was wrong most of the time, how did the system make any progress at all?&lt;/p&gt;

&lt;p&gt;The answer is uncomfortable: it made progress &lt;em&gt;despite&lt;/em&gt; the gate, not because of it. A typical failure looked like this. The first attempt satisfied the tests but used a structure the gate distrusted, so it got blocked. The retry didn't improve the behavior — it just reshaped the same behavior into a form the gate would accept. From the dashboard, that looked like "the gate forced an improvement." From the case review, it was adaptation to the gate. (When even that didn't work, I'd step in and wave the work through, because I could see it was fine — another quiet sign the gate wasn't earning its place.)&lt;/p&gt;

&lt;p&gt;That was the tell I'd missed. A gate doing real work makes the loop converge on &lt;em&gt;better&lt;/em&gt; code. Mine was making the loop converge on &lt;em&gt;gate-shaped&lt;/em&gt; code — which is not the same thing, and is sometimes worse. The retry didn't make the code more correct. It made it more acceptable to the gate.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I now try to tell a real block from a false one
&lt;/h2&gt;

&lt;p&gt;Catching this forced me to write down what would actually distinguish a justified block from a noisy one. The count clearly wasn't it. What I landed on is a three-part check — not elegant, but it's caught things since, so I'll offer it as a working heuristic for this setup rather than a rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Look at the distribution of reasons, not the total.&lt;/strong&gt; I'd expect the block reasons to map to substantive defects, not to repeatedly trip on the same shallow surface feature regardless of whether the work is good. If they cluster on the latter, the gate is probably pattern-matching on the wrong thing instead of judging quality. (This is more useful for a broad quality gate than for a narrow, single-purpose check.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Watch what happens on retry.&lt;/strong&gt; This turned out to be the most useful signal. In my loop, a &lt;em&gt;justified&lt;/em&gt; block tended to make the work stick on the same underlying defect across retries, until that defect was actually addressed; a &lt;em&gt;false positive&lt;/em&gt; produced shape-shifting attempts that changed the surface without improving the behavior. It's a tendency, not a law — a model can wander even when the defect is real — but the shape of the retry sequence carried information the single block event didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Check final convergence.&lt;/strong&gt; A justified block should eventually &lt;em&gt;resolve&lt;/em&gt;: the work gets rewritten, the real problem gets fixed, and it passes on its own merits. If blocked work never converges — or only "passes" once you weaken the gate — then either the gate was wrong, or it was right and your loop can't act on it. Both are problems, and both are invisible if you only count how many times it fired.&lt;/p&gt;

&lt;p&gt;None of these is a clean pass/fail on its own. Together they let me ask the question I'd skipped — &lt;em&gt;was this block justified?&lt;/em&gt; — instead of reading the answer off the fact that a block happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The deeper version of the mistake
&lt;/h2&gt;

&lt;p&gt;There's a more general trap underneath this, and I want to name it plainly, because I fell into it without noticing.&lt;/p&gt;

&lt;p&gt;A test that only checks "the gate blocks bad input" is testing the easy half. The hard half is: does the gate let through good input that simply looks unusual? If you only ever feed a guard the inputs it's supposed to reject, of course it rejects them, and of course the tests pass — but you've proven nothing about its false-positive behavior, which is exactly where mine was failing. The gate's own tests were green for the same reason the gate looked healthy: I'd only ever asked it the flattering question.&lt;/p&gt;

&lt;p&gt;So now, when I test a gate, I deliberately include cases that are legitimate but oddly shaped — valid work in a form the gate might naively distrust — and check that it lets them through. In this case, the negative cases (reject the bad stuff) were the easier half. The risk I'd under-tested was the good stuff in unfamiliar clothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this didn't prove
&lt;/h2&gt;

&lt;p&gt;I don't want to inflate this into "all gates are bad" or "block counts are meaningless." Neither is true.&lt;/p&gt;

&lt;p&gt;Gates can earn their keep. A well-calibrated one really does stop real problems, and the block count is a perfectly good &lt;em&gt;operational&lt;/em&gt; signal — useful for noticing that something is happening, or spotting a sudden spike. The mistake wasn't tracking the count. It was treating the count as evidence of &lt;em&gt;correctness&lt;/em&gt; when it's only evidence of &lt;em&gt;activity&lt;/em&gt;. Those are different axes, and I'd conflated them.&lt;/p&gt;

&lt;p&gt;I'd also flag that my three-part check is shaped by this particular system — a test-driven loop where blocked work gets automatically retried, so "watch the retries" is even available to me as a signal. If your setup doesn't produce that kind of trajectory, parts of this won't transfer. I'm offering it as something that worked in one place, not a general theorem about guards. I've been wrong about generality before in this project, so I'm holding it loosely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway, stated honestly
&lt;/h2&gt;

&lt;p&gt;A guard blocking something tells you it's &lt;em&gt;active&lt;/em&gt;. It does not tell you it's &lt;em&gt;right&lt;/em&gt;. Those are separate facts, and the gap between them is where a confident-looking gate can quietly turn into a noise machine that punishes good work and calls it protection.&lt;/p&gt;

&lt;p&gt;If you run gates, linters, validators, policy filters — anything that stops things — it's worth auditing a &lt;em&gt;sample of what it blocked&lt;/em&gt;, not just the total it blocked. The total can easily look like diligence. The sample is where you find out whether the diligence was real.&lt;/p&gt;




&lt;p&gt;I'm curious how others handle this. If you operate a gate or a strict CI rule, do you ever sample its blocks to check they were justified — and if so, how do you decide "justified" without it turning subjective? I worked out a rough method for my case, but it leans on my system's particular shape, and I'd like to hear how it's done elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next in the series: I asked an AI agent to count something for me. It said 12. The real number was 13 — and that one-off gap changed a rule I now follow.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>When pytest Said "Passed," It Was Lying</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Sat, 20 Jun 2026 13:05:33 +0000</pubDate>
      <link>https://dev.to/josephyeo/when-pytest-said-passed-it-was-lying-43p7</link>
      <guid>https://dev.to/josephyeo/when-pytest-said-passed-it-was-lying-43p7</guid>
      <description>&lt;h2&gt;
  
  
  How a polluted virtual environment made my green tests meaningless
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Part of the ForgeFlow series — building a coding agent that runs its execution loop locally on an M5 Max, and writing down what actually breaks. Planning runs on Claude; code generation runs on a local model via Ollama, test-driven inside a Docker sandbox.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;For a few days, I made decisions on top of a number that wasn't true.&lt;/p&gt;

&lt;p&gt;The number was &lt;code&gt;186 passed&lt;/code&gt;. It came out of pytest, green, at the bottom of the terminal, the way it had dozens of times before. I trusted it the way you trust a number that has never been wrong before. Then I found out the run had been measured inside the wrong environment, and the green had very little to do with the code I thought I was checking.&lt;/p&gt;

&lt;p&gt;To be fair to the tool: pytest wasn't wrong. It answered exactly the question I handed it — it just wasn't the question I meant to ask. This post is about that gap. Not a bug in a test, but a bug in &lt;em&gt;how I measured the tests&lt;/em&gt;. It turned out to be one of the more uncomfortable lessons in the project so far, because it sat underneath everything else. If the floor is tilted, every measurement you take on top of it inherits the tilt, and you don't see it, because the floor looks like the floor.&lt;/p&gt;

&lt;p&gt;I'm writing it down mostly because I suspect I'm not the only person who has trusted a green checkmark that didn't earn it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup: a baseline I checked constantly
&lt;/h2&gt;

&lt;p&gt;ForgeFlow is a coding agent that runs a test-driven loop. Plan, write a failing test, write code, run the tests, decide what happened, repeat. Because so much of the system's behavior is judged by "did the tests pass," I keep a &lt;strong&gt;baseline&lt;/strong&gt;: a known set of test files that should report a known set of numbers. Before and after almost any change to the engine, I re-run the baseline and compare. If the counts move when they shouldn't, something is wrong.&lt;/p&gt;

&lt;p&gt;One thing to be precise about, because it matters later: the agent &lt;em&gt;executes the code it generates&lt;/em&gt; inside a Docker sandbox, but this baseline — the engine's own test suite — I was running from my host shell. Two different executions. The sandbox was fine. The host shell was the problem.&lt;/p&gt;

&lt;p&gt;The baseline is the closest thing the project has to a source of truth about its own health. That's exactly why this hurt.&lt;/p&gt;

&lt;p&gt;One afternoon I was moving between two things on the same machine — the agent's own codebase, and an unrelated project I'd been poking at earlier. I ran the baseline. Green. &lt;code&gt;186 passed&lt;/code&gt;. I noted it, moved on, and built the next decision on top of it.&lt;/p&gt;

&lt;p&gt;What I didn't notice was a single line of state that had carried over from the earlier work.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;I'd left a different project's virtual environment active.&lt;/p&gt;

&lt;p&gt;That's the whole bug, mechanically. The shell still had another project's &lt;code&gt;VIRTUAL_ENV&lt;/code&gt; set, so when I ran pytest, it resolved &lt;code&gt;pytest&lt;/code&gt; through &lt;em&gt;that&lt;/em&gt; environment, and Python resolved imports against that environment's installed packages.&lt;/p&gt;

&lt;p&gt;Here's the question a careful reader asks immediately: if it was the wrong environment, why didn't it just fail? Why no &lt;code&gt;ModuleNotFoundError&lt;/code&gt;, no loud red collection error?&lt;/p&gt;

&lt;p&gt;Because nothing was missing. The polluted environment happened to have the same packages installed — only at different versions. So nothing errored out. The tests collected, ran, and passed; they just passed against a version matrix the baseline doesn't assume. And that's the genuinely unsettling part: &lt;strong&gt;if a dependency had been entirely absent, I'd have gotten a loud error and caught it in seconds.&lt;/strong&gt; The danger was precisely that everything was &lt;em&gt;present&lt;/em&gt; — present and subtly wrong. Wrong in the one way that doesn't announce itself.&lt;/p&gt;

&lt;p&gt;The problem is that "green" had stopped meaning what I read it as. I read it as &lt;em&gt;"the code is correct in the environment it's meant to run in."&lt;/em&gt; What it actually meant was &lt;em&gt;"the code passed in whatever environment happened to be active."&lt;/em&gt; Those are different sentences. For a few days I couldn't tell them apart, because the terminal prints the same word for both.&lt;/p&gt;

&lt;p&gt;Here's the part worth sitting with: &lt;strong&gt;nothing failed.&lt;/strong&gt; A failing test is a gift — it's loud, it points at itself, you go fix it. This didn't fail. It passed, and the passing was the problem. The signal I rely on to catch mistakes was itself the mistake, wearing the costume of everything being fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  "The tests pass" and "the measurement is honest" are different claims
&lt;/h2&gt;

&lt;p&gt;When I finally caught it — by comparing against a run from a fresh shell using the project's intended environment, and noticing the counts didn't line up — I reduced the lesson to a sentence I've kept since:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Whether the tests pass and whether the &lt;em&gt;measurement of the tests is trustworthy&lt;/em&gt; are two separate questions, and I had been treating them as one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Almost all of my testing discipline had been aimed at the first question. I had careful tests. What I didn't have was anything checking the second — the integrity of the act of measuring. The environment the measurement runs in is an input to the result, and I'd been treating it as a constant when it was actually a variable I'd left lying around.&lt;/p&gt;

&lt;p&gt;It's easy to invest heavily in test &lt;em&gt;correctness&lt;/em&gt; while leaving test &lt;em&gt;measurement integrity&lt;/em&gt; implicit — to treat it as the environment's job, or the tooling's job, rather than something to check directly. Plenty of teams do handle it, with lockfiles, containerized test runs, hermetic builds. I just wasn't one of them at this layer, for this particular command, on this particular day.&lt;/p&gt;




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

&lt;p&gt;Two things, deliberately small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A guard before the measurement, not just inside it.&lt;/strong&gt; The cheapest fix is a single check that runs before the baseline: confirm the environment is the one I think it is. In my case, simply asserting that no foreign virtual environment was active would have caught it — the testing equivalent of checking the floor before you measure the wall. It's almost embarrassingly simple, and it would have caught this in one second. (A stricter guard wouldn't stop at the &lt;code&gt;VIRTUAL_ENV&lt;/code&gt; variable; it would also check &lt;code&gt;sys.executable&lt;/code&gt;, the resolved &lt;code&gt;pytest&lt;/code&gt; path, and the expected project root. The variable was the obvious giveaway here, but it's the weakest of the four.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A separate set of checks for the measurement itself.&lt;/strong&gt; Beyond the one-line guard, I added a small, dedicated set of tests whose only job is to protect the &lt;em&gt;invariants of how I measure&lt;/em&gt; — not the features, the measurement. They're counted separately from the normal baseline on purpose, so they can't be quietly folded into the same number they're supposed to be watching. The exact count is secondary. What matters is that "is my measurement honest" became something the system checks for me, instead of something I assume.&lt;/p&gt;

&lt;p&gt;Neither of these is clever. That's sort of the lesson. The failure wasn't subtle once I saw it; it was invisible only because I'd never thought to look there.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this didn't prove
&lt;/h2&gt;

&lt;p&gt;I want to be careful not to inflate this into a grand principle.&lt;/p&gt;

&lt;p&gt;This is one incident, on one machine, caused by one careless bit of leftover state. It doesn't prove that everyone's test suites are secretly lying, and it doesn't prove you need an elaborate measurement-verification layer. For a small throwaway script, a clean shell and a moment of attention is the entire fix, and the machinery would be overkill.&lt;/p&gt;

&lt;p&gt;To be clear about which part scales: environment hygiene matters at any size — it's the &lt;em&gt;guards and dedicated checks&lt;/em&gt; that scale with how much you're betting on the number. I'm betting a lot on mine; the agent makes real decisions off these signals. So for me the machinery earned its place. The hygiene would have been worth it regardless of project size.&lt;/p&gt;

&lt;p&gt;I'm also aware the "fix" mostly moves the trust down one level. Now I trust the guard. If the guard is wrong, I'm back where I started. There's no absolute bottom here — just a level low enough that I'm willing to stop and call it ground. I picked one. I could be wrong about whether it's low enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  The takeaway, stated honestly
&lt;/h2&gt;

&lt;p&gt;If I had to compress it: a green test run answers "did the code pass?" It does &lt;em&gt;not&lt;/em&gt; answer "did I measure that in the environment I meant to?" The second question has its own failure mode, and because the failure mode is &lt;em&gt;passing&lt;/em&gt;, your normal instincts — chase the red, fix what's loud — never fire.&lt;/p&gt;

&lt;p&gt;The verification pyramid most of us picture has tests at the bottom. I'd now put one more layer underneath it: &lt;em&gt;the environment the tests run in.&lt;/em&gt; When that layer shifts, every green light above it is reporting on an environment you're not actually in.&lt;/p&gt;




&lt;p&gt;I'd like to know how other people handle this. Do you guard your test environment explicitly, or rely on convention and attention? Have you been bitten by a &lt;em&gt;passing&lt;/em&gt; result that turned out to be measured wrong — and if so, how did you finally catch it? I caught mine by luck and a mismatched count. I'd rather not depend on luck next time, and I suspect some of you have better answers than I do.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next in the series: a quality gate in the same system blocked code 198 times — and why I was wrong to call that "working."&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>We Spent Six Sessions Fixing One Task. The Problem Was Six Tasks.</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Tue, 02 Jun 2026 11:42:06 +0000</pubDate>
      <link>https://dev.to/josephyeo/we-spent-six-sessions-fixing-one-task-the-problem-was-six-tasks-385d</link>
      <guid>https://dev.to/josephyeo/we-spent-six-sessions-fixing-one-task-the-problem-was-six-tasks-385d</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 9 of the ForgeFlow series. &lt;a href="https://dev.to/josephyeo/77-rules-later-what-graduating-our-first-stack-actually-looked-like-2o3k"&gt;Part 8: 77 Rules Later&lt;/a&gt; ended on a question we couldn't answer at the time: can a rule-based agent system keep growing without becoming harder to reason about than the model it was built to constrain? This post is about one small episode that pushed us toward an answer — not a clean one, but a useful one. It's also a record of getting a diagnosis wrong for several sessions in a row, and what finally corrected it.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick terms for new readers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FC&lt;/strong&gt; = Failure Catalog entry (a documented failure pattern)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CL&lt;/strong&gt; = Crystallized Lesson (a testable design rule derived from repeated failures)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;critical_rules&lt;/strong&gt; = a block of rules injected into the model's prompt for a given project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nightrun&lt;/strong&gt; = an overnight batch that re-runs projects so we can measure behavior over many runs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ForgeFlow&lt;/strong&gt; = a fully local, TDD-based autonomous coding system running on Apple Silicon&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The failure we kept "fixing"
&lt;/h2&gt;

&lt;p&gt;For several sessions, we had a recurring failure in one project that we treated as a single, local problem.&lt;/p&gt;

&lt;p&gt;The symptom: in async tests, the model would construct an HTTP test client directly instead of using the shared fixture we'd set up. That shared fixture is what installs the dependency override pointing the app at the test database. By hand-rolling its own client, the model skipped the fixture entirely — so the override was never applied, and the test hit an unconfigured database and failed with a missing-table error.&lt;/p&gt;

&lt;p&gt;(The underlying trigger, for those on the same stack: &lt;code&gt;httpx&lt;/code&gt; 0.28 removed the deprecated &lt;code&gt;app=&lt;/code&gt; shortcut from &lt;code&gt;Client&lt;/code&gt;/&lt;code&gt;AsyncClient&lt;/code&gt;. The supported pattern is now &lt;code&gt;transport=ASGITransport(app=app)&lt;/code&gt;. Our conftest fixture already used the current &lt;code&gt;ASGITransport&lt;/code&gt; approach and was correct — the &lt;code&gt;app=&lt;/code&gt; argument on &lt;code&gt;ASGITransport&lt;/code&gt; itself is still valid. The model just wasn't using the fixture; it kept hand-rolling the client the old, removed way. Why it preferred the old pattern is something we can only guess at — likely the weight of older examples — and we didn't try to prove it.)&lt;/p&gt;

&lt;p&gt;Each time it surfaced, we did the natural thing. We looked at the task where it appeared, and we wrote a rule for &lt;em&gt;that task&lt;/em&gt;. Next run, it would seem quieter there — and then show up somewhere else. We'd note it, half-suspect it was the same thing, and move on to whatever else the run surfaced.&lt;/p&gt;

&lt;p&gt;What we were doing, in effect, was treating a recurring pattern as a series of unrelated incidents. We never asked the obvious question: &lt;em&gt;how often does this actually happen, and where?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement we should have taken earlier
&lt;/h2&gt;

&lt;p&gt;The thing that broke the loop wasn't a better model or a smarter rule. It was a measurement we hadn't bothered to take.&lt;/p&gt;

&lt;p&gt;We wrote a small script to walk back through our run history and count occurrences of the failing pattern across every recorded run — not by reconstructing it from summary tables, but by grepping the raw captured error text directly. (We'd learned in an earlier session that our summary-level deduplication could merge distinct failures under one signature, so for this we went to the raw text instead.) In practice that meant grepping the archived &lt;code&gt;stderr&lt;/code&gt; for the exact failing construction across every run directory, rather than trusting the rolled-up signatures.&lt;/p&gt;

&lt;p&gt;In the archived runs we checked, the count came back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;16 occurrences&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;across &lt;strong&gt;6 distinct tasks&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;in &lt;strong&gt;3 different projects&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;spread over &lt;strong&gt;7 separate run sessions&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This count made our earlier framing untenable. We had been writing single-task rules for something that was happening across six tasks in three projects. Each of those per-task fixes had been addressing, at most, one of the sixteen occurrences we eventually counted. The measurement didn't just refine our picture — for this failure pattern, it showed our framing had been wrong in kind, not just in degree.&lt;/p&gt;

&lt;p&gt;It's a little uncomfortable to write that down. But that's the part worth keeping: the wrong number wasn't in the model's output. It was in our own estimate of how widespread the problem was, and we'd carried that estimate for several sessions without checking it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prescription got smaller, not bigger
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprised us most.&lt;/p&gt;

&lt;p&gt;Once we saw "6 tasks, 3 projects," the instinct might be to write six fixes — one hardened rule per affected task. Given how we'd been responding up to that point, that's roughly the path we were on.&lt;/p&gt;

&lt;p&gt;Instead, the data pointed the other way. If one pattern was appearing across many tasks and projects, the better place to address it wasn't any individual task — it was the project-wide rule block. We added &lt;strong&gt;one line&lt;/strong&gt; to the &lt;code&gt;critical_rules&lt;/code&gt; for the affected projects: a single instruction telling the model not to construct the test client directly, and to use the fixture instead (taking the rule block from 20 lines to 21).&lt;/p&gt;

&lt;p&gt;One rule addressed a pattern that, on our prior trajectory, could easily have become six separate task-level patches. This was a small, concrete instance of something we keep seeing on this project: when we measure a problem's actual scope more carefully, the fix tends to get narrower, not wider. When you don't know the shape of a problem, you tend to over-prescribe locally and under-address globally. Measuring the shape let us do less.&lt;/p&gt;

&lt;p&gt;We then ran a verification batch over the affected projects. The grep count for the pattern came back at &lt;strong&gt;zero&lt;/strong&gt; across those runs. Two of the projects involved — a small gallery API and a small library API — met the per-project completion criteria on the verifying runs, so we marked them as "graduated" in a narrow, project-level sense.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on "graduated":&lt;/strong&gt; Part 8 used this word for an entire stack. Here it means something much smaller — these individual projects met their completion criteria on the runs we executed. It is not a claim about the stack, and not a claim that these projects will never fail again. The underlying numbers were modest: pass rates in the roughly 38–85% range across individual runs. What changed wasn't a jump to near-perfect runs — it was that this particular structural failure stopped appearing. We're reporting a state we observed, not a guarantee.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What this didn't prove
&lt;/h2&gt;

&lt;p&gt;A few limits, because the result is smaller than it might sound.&lt;/p&gt;

&lt;p&gt;The zero is a zero &lt;em&gt;on the runs we executed&lt;/em&gt;, for &lt;em&gt;this specific pattern&lt;/em&gt;. It's evidence the rule is doing its job in the tested scope, not proof the pattern is gone for good. A different project, a different &lt;code&gt;httpx&lt;/code&gt; version, or a different phrasing of the same task could surface it again.&lt;/p&gt;

&lt;p&gt;The measurement approach itself has a known weakness we worked around rather than solved: it reads raw error text, which is reliable for exact-pattern counting but says nothing about &lt;em&gt;why&lt;/em&gt; each occurrence happened. For this pattern — a single, well-understood cause — that was fine. For a fuzzier failure, raw-text counting would undercount or overcount, and we'd need something better.&lt;/p&gt;

&lt;p&gt;And the broader idea ("measure before you prescribe") is a working heuristic from repeated experience on this project, not something we've established rigorously. We'd genuinely like to know where it breaks down for other people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond the coding loop
&lt;/h2&gt;

&lt;p&gt;The same week, an unrelated problem came up — a structural issue in how some of our project directories were tracked in version control. An earlier note had flagged it as a likely large cleanup, the kind of thing you budget a careful session for.&lt;/p&gt;

&lt;p&gt;Before touching anything, we applied the same lesson the measurement script had just taught us: measure first. A few read-only checks showed the actual scope was much smaller than the earlier flag assumed — most of what looked like a structural defect turned out to be a single missing ignore-rule and one stale index entry, and the fix was a handful of non-destructive steps rather than a restructuring. I'm including this not as a second result but as an observation: in both cases the failure mode was the same — acting on an estimate instead of a measurement — and in both cases, once we measured, the prescription shrank. Two instances isn't a trend, but it was enough to make "measure the scope before writing the fix" a step we now try to take on purpose rather than when we happen to remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to Part 8's question
&lt;/h2&gt;

&lt;p&gt;Part 8 ended by asking whether a growing rule set could stay manageable. This episode is one data point toward a tentative answer: rule growth seems more controllable when the &lt;em&gt;decision to add a rule&lt;/em&gt; is driven by measured scope rather than by where a failure last happened to appear. Six task-level rules would have grown the rule set faster and addressed the problem worse. One measured rule did more with less.&lt;/p&gt;

&lt;p&gt;That's not a method, and it's certainly not a solution to the interaction-effect problem from Part 8. It's a habit we're trying to form: before crystallizing a new rule, spend the few minutes it takes to count how often and where the underlying failure actually occurs. Sometimes that collapses six patches into one rule. Other times — we assume, though we haven't hit this yet — it will reveal that what looked like one problem is really several, and we'll need more rules, not fewer. Either way, the rule follows the measurement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/i-built-a-local-ai-coding-agent-on-m5-max-128gb-it-failed-164-times-before-passing-35-tests-2fgj"&gt;Part 1: 164 Failures Before 35 Tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/we-didnt-migrate-from-n8n-to-python-because-n8n-failed-k9j"&gt;Part 2: We Didn't Migrate from n8n Because n8n Failed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/the-determinism-war-why-we-stopped-chasing-better-models-3c21"&gt;Part 3: The Determinism War&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/the-information-design-gap-why-our-ai-agent-was-coding-blind-4p8o"&gt;Part 4: The Information Design Gap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/dcr-wasnt-enough-why-ai-coding-agents-also-need-information-quality-1da4"&gt;Part 5: DCR Wasn't Enough&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/the-bug-wasnt-in-the-model-lessons-from-9-local-ai-coding-agent-projects-18aa"&gt;Part 6: The Bug Wasn't in the Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/the-file-modification-boundary-we-found-after-12-forgeflow-projects-3m01"&gt;Part 7: The File Modification Boundary&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/77-rules-later-what-graduating-our-first-stack-actually-looked-like-2o3k"&gt;Part 8: 77 Rules Later&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;ForgeFlow runs on a MacBook Pro M5 Max 128GB. Planning uses Claude (cloud API). Execution is fully local — Qwen3-Coder-Next 45GB via Ollama, gemma4:26b for QA, Docker sandbox, no API calls during the coding loop. The methodology and failure data are shared in this series.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're running your own local agents or failure-catalog systems: have you caught yourself prescribing locally for a problem that turned out to be project-wide — and what finally made you measure it? Logs, test signatures, prompts, generated diffs? The comments are open.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>devjournal</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>77 Rules Later: What Graduating Our First Stack Actually Looked Like</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Mon, 25 May 2026 08:19:22 +0000</pubDate>
      <link>https://dev.to/josephyeo/77-rules-later-what-graduating-our-first-stack-actually-looked-like-2o3k</link>
      <guid>https://dev.to/josephyeo/77-rules-later-what-graduating-our-first-stack-actually-looked-like-2o3k</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 8 of the ForgeFlow series. &lt;a href="https://dev.to/josephyeo/the-file-modification-boundary-we-found-after-12-forgeflow-projects-3m01"&gt;Part 7: The File Modification Boundary&lt;/a&gt; documented the constraint that changed how we structure tasks: every autonomous task target should be a new file. We ended Part 7 at 12 projects, roughly 52 failure patterns, and 71 design rules. Part 7 closed with an open question: "Project 13 will be the first real test of whether CL-071 holds under normal conditions."&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick terms for new readers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FC&lt;/strong&gt; = Failure Catalog entry (a documented failure pattern)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CL&lt;/strong&gt; = Crystallized Lesson (a testable design rule derived from repeated failures)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEADLOCK&lt;/strong&gt; = the system gives up after repeated identical failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ForgeFlow&lt;/strong&gt; = a fully local, TDD-based autonomous coding system running on Apple Silicon&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;Part 7 ended with a hypothesis and a bet.&lt;/p&gt;

&lt;p&gt;The hypothesis: CL-071 (every task targets a new file, never modifies an existing one) might reduce or remove the dominant failure mode we'd been observing. The bet: we'd set formal graduation criteria and run projects until we met them — or discovered why we couldn't.&lt;/p&gt;

&lt;p&gt;We ran five more projects (with one intermediate rerun included in the data). On the seventeenth — a blog API with 14 tasks — all 33 tests passed without intervention or deadlock, completing in approximately 12 minutes.&lt;/p&gt;

&lt;p&gt;This post is about the five projects between that hypothesis and this result, what the graduation criteria actually measured, and the failure that appeared &lt;em&gt;after&lt;/em&gt; we thought we'd addressed all the known ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Graduation Criteria
&lt;/h2&gt;

&lt;p&gt;Before results, here's what we were measuring. We didn't want "it worked once" to count as graduation. We defined four conditions, all of which had to hold on a qualifying run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First-run pass rate (tasks passing on the first TDD cycle, no retry)&lt;/td&gt;
&lt;td&gt;≥ 85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New FC yield per project&lt;/td&gt;
&lt;td&gt;≤ 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeat FC rate (previously solved patterns recurring)&lt;/td&gt;
&lt;td&gt;≤ 5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teacher escalation (human operator interventions mid-task)&lt;/td&gt;
&lt;td&gt;Decreasing trend&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The logic: a graduated stack should show repeatable autonomous recovery within the tested scope (criterion 1), stop producing novel failure patterns at a high rate (criterion 2), not regress on already-solved problems (criterion 3), and require less human involvement over time (criterion 4).&lt;/p&gt;

&lt;p&gt;We chose 85% rather than 100% for the pass rate deliberately. Occasional retries are expected behavior in a TDD loop — in ForgeFlow's architecture, the system is designed to recover from them. What we track is whether it recovers autonomously.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Five-Project Path
&lt;/h2&gt;

&lt;p&gt;Here's the longitudinal data from Part 7's endpoint (project 12) through the graduation run. Note: this table tracks the &lt;em&gt;autonomous pass rate&lt;/em&gt; — tasks that eventually passed without human intervention, including retries. The graduation criterion uses the stricter &lt;em&gt;first-run pass rate&lt;/em&gt; (no retries), which we measured separately for the qualifying run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Tasks&lt;/th&gt;
&lt;th&gt;Autonomous Pass Rate&lt;/th&gt;
&lt;th&gt;New FCs&lt;/th&gt;
&lt;th&gt;CL Count (at time)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;comment-api&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;~72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;order-api&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;~74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;recipe-api&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;57%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~75&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;bookmark-api v2&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;~76&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16.5&lt;/td&gt;
&lt;td&gt;catalog-api-v2&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;83%&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~76&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;17&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;blog-api&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;77&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trajectory wasn't smooth. Projects 14 and 15 dropped below 60%. Then it recovered. In this sequence, plateaus tended to expose a new failure category; the system dipped, the failure got crystallized into a rule, and the next project incorporated the fix.&lt;/p&gt;

&lt;p&gt;What changed between project 15 (57%) and project 17 (100%) was not a model upgrade or an engine rewrite. It was three additional design rules, each derived from a specific failure we observed and diagnosed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dip: What Went Wrong on Projects 14 and 15
&lt;/h2&gt;

&lt;p&gt;Projects 14 (order-api) and 15 (recipe-api) both hovered around 56–57% autonomous pass rate. The failures clustered around a few patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Route endpoint isolation.&lt;/strong&gt; Tasks that bundled multiple endpoints into a single file — GET list and GET detail in the same route module — showed a notably higher failure rate than single-endpoint tasks. The outputs showed scope-related failures: given two endpoints to implement, the model would sometimes complete one and leave the other as a stub, or attempt both and introduce inconsistencies.&lt;/p&gt;

&lt;p&gt;We already had CL-043 (one task, one endpoint) from Part 6. But we'd been applying it loosely — allowing two closely related endpoints to share a task. Projects 14 and 15 showed us that "closely related" was too vague for this local execution loop. The rule needed to be absolute: one endpoint, one file, one task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Import specification gaps.&lt;/strong&gt; Route tasks that didn't explicitly list every required import in their task description had a high failure rate. The model would guess import paths, often incorrectly. CL-072 crystallized this: every route task description must include a complete "Required imports" block. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;Required&lt;/span&gt; &lt;span class="n"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.ext.asyncio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.schemas.author&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthorCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AuthorRead&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Decimal type mismatches.&lt;/strong&gt; In project 16.5 (catalog-api-v2), a product model with a &lt;code&gt;Numeric(10,2)&lt;/code&gt; price column exposed a subtle testing issue. The model wrote assertions comparing float literals to SQLAlchemy Decimal values — and &lt;code&gt;999.99 != Decimal('999.99')&lt;/code&gt; in Python. CL-076 captured this: any Numeric column test must use Decimal comparisons.&lt;/p&gt;

&lt;p&gt;In our diagnosis, these looked less like model-capability failures and more like specification-precision failures — cases where the PRD left enough ambiguity for a 45GB quantized model to make a reasonable-but-wrong choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Failure We Didn't Expect: FC-074
&lt;/h2&gt;

&lt;p&gt;Project 17 (blog-api) was designed as the graduation attempt. We applied all 76 existing rules. The PRD passed our automated validator (50 checks passed, 0 failures). We expected fewer known-pattern failures.&lt;/p&gt;

&lt;p&gt;The first three attempts all failed on the very first task — creating the Author model. Same error each time: &lt;code&gt;red_apply_empty&lt;/code&gt; — the engine's signal that the RED-phase output contained implementation code rather than a test.&lt;/p&gt;

&lt;p&gt;Here's what happened, step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Our setup script created a minimal model stub file — just the class name and primary key column. This was standard practice per CL-066 ("stubs should be PK-only").&lt;/li&gt;
&lt;li&gt;Before the RED phase (test generation), the engine runs FC-060 cleanup: it deletes the target implementation file so the model writes it fresh.&lt;/li&gt;
&lt;li&gt;FC-060 deleted the stub.&lt;/li&gt;
&lt;li&gt;The model didn't need the file to exist at generation time — the surrounding task context still described enough of the intended model structure (via data_models in the PRD and conftest import references) that it produced implementation code during RED instead of a test.&lt;/li&gt;
&lt;li&gt;The engine detected this as a scope violation and triggered &lt;code&gt;red_apply_empty&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Three retries. Same result each time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We called this FC-074: the interaction between two previously validated rules (CL-066: keep stubs minimal, and FC-060: clean target files before RED) producing a new failure when combined.&lt;/p&gt;

&lt;p&gt;This is worth pausing on. FC-074 wasn't a gap in any single rule. It was an &lt;em&gt;interaction effect&lt;/em&gt; — two rules that had each been validated independently across multiple projects, producing a failure only in a specific sequence of operations.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Behavior in isolation&lt;/th&gt;
&lt;th&gt;Combined behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CL-066&lt;/td&gt;
&lt;td&gt;Minimal stubs reduce over-complete-stub failures&lt;/td&gt;
&lt;td&gt;Creates a target file before RED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FC-060&lt;/td&gt;
&lt;td&gt;Deletes implementation target before RED to ensure clean state&lt;/td&gt;
&lt;td&gt;Removes the stub CL-066 created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combined&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;RED sees a missing target but enough context to generate implementation instead of a test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Fix: Stop Creating Stubs
&lt;/h2&gt;

&lt;p&gt;The first instinct was to adjust the prompt wording — tell the model more explicitly to write a test, not an implementation. We tried that. Same failure. Prompt changes alone didn't resolve it; file-state became the stronger hypothesis.&lt;/p&gt;

&lt;p&gt;The second instinct was to refine the stub. But we diagnosed the stub's existence as the likely trigger: FC-060 deleted it, and the residual context information was enough to derail the RED phase.&lt;/p&gt;

&lt;p&gt;The third attempt was the simplest: don't create the stub at all.&lt;/p&gt;

&lt;p&gt;CL-077: Setup scripts must not create model stub files. Model files are created from scratch by the task that implements them. The conftest wraps model imports in try/except so that earlier tasks can run before the model file exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.models.author&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Author&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;Author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This inverted an assumption we'd held across the previous 16 project iterations. We'd operated under the belief that providing a stub — even a minimal one — helped the model by giving it a starting point. FC-074 suggested that in our current engine architecture, the stub &lt;em&gt;hurt&lt;/em&gt; by creating a state that the cleanup logic couldn't handle cleanly.&lt;/p&gt;

&lt;p&gt;After applying CL-077, the same blog-api project ran all 14 tasks to completion. 33 tests passed, zero intervention, approximately 12 minutes total.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Graduation Run Measured
&lt;/h2&gt;

&lt;p&gt;Here's how project 17 scored against the criteria:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Project 17 Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First-run pass rate&lt;/td&gt;
&lt;td&gt;≥ 85%&lt;/td&gt;
&lt;td&gt;93% (13/14 first-shot, 1 retry)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New FC yield&lt;/td&gt;
&lt;td&gt;≤ 2&lt;/td&gt;
&lt;td&gt;1 (FC-074)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeat FC rate&lt;/td&gt;
&lt;td&gt;≤ 5%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teacher escalation&lt;/td&gt;
&lt;td&gt;Decreasing&lt;/td&gt;
&lt;td&gt;Zero escalations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Project 17 met all four thresholds. The preceding project (16.5, catalog-api-v2) reached 83% — close but below the ≥85% line. So we are treating project 17 as the graduation point rather than claiming a two-project stable plateau.&lt;/p&gt;

&lt;p&gt;To be precise about what this means and what it doesn't:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it means:&lt;/strong&gt; On the specific runs we executed — FastAPI + SQLAlchemy async + pytest projects with CRUD-level complexity and 1:N foreign key relationships, using Qwen3-Coder-Next 45GB Q4_K_M on Apple Silicon M5 Max 128GB with 77 design rules — the system completed the full project autonomously within the scope of new-file-creation tasks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't mean:&lt;/strong&gt; We haven't tested more complex architectural patterns (many-to-many relationships, authentication flows, file uploads, WebSocket endpoints). We haven't tested with different model families or hardware tiers. The 100% figure is for one specific project run; it's a data point, not a guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;77 rules is a lot of rules.&lt;/strong&gt; Each one was derived from at least one observed problem. But the cumulative load of maintaining 77 interacting rules is substantial. We don't yet know if this scales — whether a 200-rule system would be manageable or would collapse under interaction effects. This matches a concern we are starting to track internally: beyond a certain threshold, adding more constraints may dilute model attention rather than improve output. In our design, we've set a ceiling of 20 CLs per prompt injection bundle to guard against this, but we haven't yet hit a project that tests that limit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rule Accumulation Curve
&lt;/h2&gt;

&lt;p&gt;One pattern we've been tracking across the series is how the rate of new rule discovery changes over time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Projects  1–3:   CL-001 to CL-020   (~7 per project)
Projects  4–6:   CL-021 to CL-035   (~5 per project)
Projects  7–9:   CL-036 to CL-051   (~5 per project)
Projects 10–12:  CL-052 to CL-071   (~6 per project)
Projects 13–17:  CL-072 to CL-077   (~1 per project)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The yield dropped from roughly 7 new rules per project to roughly 1. We're cautious about reading too much into this — it could mean we're approaching the boundary of what our current project complexity can reveal, rather than the boundary of what rules exist. More complex projects might expose entirely new failure categories.&lt;/p&gt;

&lt;p&gt;But within the FastAPI + SQLAlchemy + CRUD scope, the flattening is visible in this dataset. The most notable new failure in this stretch was an interaction effect between existing rules — FC-074 — rather than an entirely novel pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Interaction Effect Problem
&lt;/h2&gt;

&lt;p&gt;FC-074 taught us something we hadn't articulated before: as the rule set grows, the opportunity for interaction effects between rules increases. Each rule is validated independently, but the system runs them all simultaneously.&lt;/p&gt;

&lt;p&gt;This resembles a familiar problem in complex systems: the space of pairwise interactions grows faster than the number of components. We can't test all combinations manually.&lt;/p&gt;

&lt;p&gt;We don't have a systematic solution for this yet. What we have is a detection mechanism: when a failure occurs that doesn't match any existing FC pattern, we now check whether it could be an interaction between two rules that had both worked in isolation in prior runs. FC-074 was caught this way.&lt;/p&gt;

&lt;p&gt;Whether this can be automated — detecting interaction effects without human diagnosis — is an open question. The engine could potentially track which CLs were active when a novel failure occurs and flag the pairwise candidates, but we haven't built that yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;Graduating from the FastAPI stack opens a question: what do we do with a graduated stack?&lt;/p&gt;

&lt;p&gt;We see two directions, each answering a different question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direction A: Complexity escalation.&lt;/strong&gt; Stay on FastAPI but increase project complexity — many-to-many relationships, authentication flows, nested resources, pagination. This tests whether the current 77 rules hold at higher complexity or whether new failure categories emerge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direction B: Stack transfer.&lt;/strong&gt; Move to a different framework and measure how many of the 77 rules transfer. Our rules are categorized by stack tags — 29 are marked "universal," 32 are "fastapi"-specific. A new stack would test whether the universal rules actually are universal.&lt;/p&gt;

&lt;p&gt;The question we're most interested in now isn't whether we can achieve another 100% run. It's whether a rule-based agent system can keep growing without becoming harder to reason about than the model it was designed to constrain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/i-built-a-local-ai-coding-agent-on-m5-max-128gb-it-failed-164-times-before-passing-35-tests-2fgj"&gt;Part 1: 164 Failures Before 35 Tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/we-didnt-migrate-from-n8n-to-python-because-n8n-failed-k9j"&gt;Part 2: We Didn't Migrate from n8n Because n8n Failed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/the-determinism-war-why-we-stopped-chasing-better-models-3c21"&gt;Part 3: The Determinism War&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/the-information-design-gap-why-our-ai-agent-was-coding-blind-4p8o"&gt;Part 4: The Information Design Gap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/dcr-wasnt-enough-why-ai-coding-agents-also-need-information-quality-1da4"&gt;Part 5: DCR Wasn't Enough&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/the-bug-wasnt-in-the-model-lessons-from-9-local-ai-coding-agent-projects-18aa"&gt;Part 6: The Bug Wasn't in the Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/josephyeo/the-file-modification-boundary-we-found-after-12-forgeflow-projects-3m01"&gt;Part 7: The File Modification Boundary&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;ForgeFlow runs on a MacBook Pro M5 Max 128GB. Planning uses Claude (cloud API). Execution is fully local — Qwen3-Coder-Next 45GB via Ollama, gemma4:26b for QA, Docker sandbox, no API calls during the coding loop. The methodology and failure data are shared in this series.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're building something similar — local AI agents, TDD automation, failure catalog systems — I'd be interested to hear whether you're seeing interaction effects between your own accumulated rules. The comments are open.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>architecture</category>
      <category>devjournal</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The File Modification Boundary We Found After 12 ForgeFlow Projects</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Fri, 22 May 2026 15:00:08 +0000</pubDate>
      <link>https://dev.to/josephyeo/the-file-modification-boundary-we-found-after-12-forgeflow-projects-3m01</link>
      <guid>https://dev.to/josephyeo/the-file-modification-boundary-we-found-after-12-forgeflow-projects-3m01</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 7 of the ForgeFlow series. &lt;a href="https://dev.to/josephyeo/the-bug-wasnt-in-the-model-lessons-from-9-local-ai-coding-agent-projects-18aa"&gt;Part 6: The Bug Wasn't in the Model&lt;/a&gt; ended at 9 projects, 51 failure patterns, and 70 design rules. Up until that point, failure rates in our setup were declining and the working framework felt like it was converging. Project 12 exposed a structural gap we hadn't yet documented.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick terms for new readers:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FC&lt;/strong&gt; = Failure Catalog entry (a documented failure pattern)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CL&lt;/strong&gt; = Crystallized Lesson (a testable design rule derived from repeated failures)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identical GREEN&lt;/strong&gt; = the model returns an unchanged file during the implementation phase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEADLOCK&lt;/strong&gt; = the system gives up after repeated identical failures&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;p&gt;Part 6 ended on a high note. Nine projects. A 100% pass rate on the last one. Forty-three crystallized lessons. A working framework in our setup: DCR × Information Quality × Task Complexity. The system felt like it was converging.&lt;/p&gt;

&lt;p&gt;Then we tried self-referential foreign keys, and a failure mode we'd only seen sporadically became the dominant pattern.&lt;/p&gt;

&lt;p&gt;This post is about project 12 — a department hierarchy API with JWT authentication and self-referential parent-child relationships. It documents the failure pattern that connected several scattered observations into a single engineering constraint. And it discusses why, in our case, the most practical response was to restructure the work rather than retry harder.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup: Department API
&lt;/h2&gt;

&lt;p&gt;Project 12 was designed to test two development vectors simultaneously: JWT authentication (new for ForgeFlow) and self-referential foreign keys (a department can be a child of another department). The tech stack was familiar — FastAPI, SQLAlchemy async, pytest — but the data model was more complex than our previous test projects.&lt;/p&gt;

&lt;p&gt;The target execution plan: 13 tasks total. Of these, 4 were new-file creation tasks (schemas, tests), 5 were existing-file modification tasks (models, routes), and 4 were either setup steps or handled outside the autonomous loop.&lt;/p&gt;

&lt;p&gt;We ran it five times, redesigning between each iteration. The pattern became hard to ignore.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Scorecard
&lt;/h2&gt;

&lt;p&gt;The table below shows the task categories from Project 12. The same outcome repeated across five redesign-and-rerun attempts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task Type&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Pass Rate&lt;/th&gt;
&lt;th&gt;Avg Cycles&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New file creation (schemas, tests)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing file modification (models, routes)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;DEADLOCK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In our setup, tasks requiring the generation of an entirely new file succeeded on the first attempt. Tasks that required modifying an existing codebase file resulted in a processing deadlock. This held across five separate runs, two different backends (direct Ollama API and Aider), and multiple retry strategies.&lt;/p&gt;

&lt;p&gt;To scope these findings: our dataset is constrained to a single model family (Qwen3-Coder-Next, 45GB Q4_K_M) running on a single hardware tier (Apple Silicon M5 Max 128GB). We don't claim these trends apply universally. But the pattern was consistent enough across five runs that we changed how we structure tasks going forward.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Identical GREEN" Looks Like
&lt;/h2&gt;

&lt;p&gt;ForgeFlow's TDD loop works in two phases: RED (write a failing test) and GREEN (write code to pass it). The GREEN phase is where modifications happen.&lt;/p&gt;

&lt;p&gt;When a task required modifying an existing file, the following loop repeated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model receives the existing file content + test requirements&lt;/li&gt;
&lt;li&gt;The model outputs code that matches the existing file exactly (detected via SHA-256 hash comparison)&lt;/li&gt;
&lt;li&gt;The engine retries with an explicit prompt: "Your output was identical to the current file"&lt;/li&gt;
&lt;li&gt;The model outputs the same file again&lt;/li&gt;
&lt;li&gt;DEADLOCK after 3 identical cycles&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We call this an &lt;em&gt;identical GREEN&lt;/em&gt; deadlock. The engine already had detection for it (FC-037, added months ago). But we'd only seen it sporadically before. In project 12, it became the primary failure mode.&lt;/p&gt;




&lt;h2&gt;
  
  
  Working Hypotheses
&lt;/h2&gt;

&lt;p&gt;We're cautious about attributing "understanding" to the model — we're observing output patterns, not internal reasoning. Here's what we think might be happening:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The whole-file generation pattern (Ollama backend):&lt;/strong&gt; When generating code via raw completion, the model streams the entire file from the first token. If the existing file is 95% correct and only needs a few lines added, the token history in the context window acts as a statistical attractor — the generation pattern defaults to reproducing the verified, working code rather than deviating to introduce new logic. The smaller the required change relative to the existing file, the stronger this pull appears to be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The diff generation constraint (Aider backend):&lt;/strong&gt; Diffs require precise line-matching tokens. When the target file is complex — multiple async routes, mixed dependencies, dense imports — generating accurate unified diff chunks appears to become erratic for our local quantized model. In our tests with this specific model and configuration, this manifested as timeouts (capped at 200 seconds per task) or a fallback to emitting an unchanged version of the source file.&lt;/p&gt;

&lt;p&gt;Both pathways showed similar limitations on file modification tasks in our configuration. Whether this is specific to quantized local models or a broader pattern, we can't say.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connecting Scattered Observations
&lt;/h2&gt;

&lt;p&gt;Before project 12, our tracker had three separate failure patterns that each captured a piece of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FC-034 / CL-043&lt;/strong&gt;: "One task, one endpoint" — adding endpoints to an existing route file often resulted in syntax errors or duplicates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FC-047 / CL-066&lt;/strong&gt;: "Over-complete stubs" — when a stub had significant boilerplate, the model treated it as finished&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FC-039 / CL-058&lt;/strong&gt;: "POST endpoints need Aider" — some tasks specifically failed on the Ollama backend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Project 12 gave us the data to connect these into a single classification, &lt;strong&gt;FC-052&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In our local execution setup, existing file modification tasks demonstrate a high probability of identical GREEN DEADLOCK on both whole-file and diff-based backends. In our observations, identical-output failures appeared more often when the required change was small relative to the existing file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From FC-052, we derived &lt;strong&gt;CL-071&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every autonomous task target should be a new file. If a workflow step must modify an existing file, that modification should either be handled programmatically during setup or the architecture should be decoupled so that features reside in isolated modules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This became our 71st crystallized lesson, and it changed how we now structure ForgeFlow projects.&lt;/p&gt;

&lt;p&gt;One notable data point: across three complete projects (10, 11, and 12), our failure catalog expanded by only a single new entry. The rule accumulation curve is flattening, which may suggest we're mapping the boundary of our current configuration — or just the boundary of our current project complexity.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Design Pattern That Emerged
&lt;/h2&gt;

&lt;p&gt;CL-071 pushed us to rethink how we write PRDs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (task-level modifications):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TASK-001: Create User model (stub)        → models/user.py
TASK-002: Add fields to User model        → models/user.py    [DEADLOCK]
TASK-003: Create Department model (stub)  → models/department.py
TASK-004: Add relationship                → models/department.py [DEADLOCK]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (decoupled new-file generation):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SETUP SCRIPT: Generate complete models with all fields and relationships
TASK-001: Create User schemas    → schemas/user.py      [NEW FILE ✅]
TASK-002: Create Dept schemas    → schemas/department.py [NEW FILE ✅]
TASK-003: Create register route  → routes/auth.py        [NEW FILE ✅]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern: infrastructure is established deterministically during setup, while the model handles clean-sheet file generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An important caveat:&lt;/strong&gt; applying this pattern to project 12 was not a clean autonomous success. We manually implemented the CRUD endpoints (6 routes) to unblock the dependency chain, then tested whether the remaining new-file task would run cleanly under the revised structure. The integration test — creating a fresh &lt;code&gt;test_integration.py&lt;/code&gt; — passed on its first autonomous cycle. The important result was narrower than "we solved it": once existing-file modification was removed from the autonomous task path, the remaining new-file task completed cleanly.&lt;/p&gt;

&lt;p&gt;We should also note an open concern: forcing every task into a "new file only" pattern shifts complexity from generation-time editing to project-level file organization. At 13 tasks, this is manageable. At 50+, it could create significant file fragmentation and import overhead. We haven't tested at that scale yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where We Are After 12 Projects
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total projects&lt;/td&gt;
&lt;td&gt;12 (11 completed, 1 scrapped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure patterns cataloged (FC)&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design rules (CL)&lt;/td&gt;
&lt;td&gt;71&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automated rule checks&lt;/td&gt;
&lt;td&gt;53 functions in validate_prd.py&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Honest Assessment
&lt;/h2&gt;

&lt;p&gt;After 12 projects and 81 sessions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's working in our setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New file generation from detailed specs: reliable across the runs we tested&lt;/li&gt;
&lt;li&gt;TDD enforcement (RED must fail, GREEN must pass): useful as a mechanical guardrail&lt;/li&gt;
&lt;li&gt;Failure pattern → design rule pipeline: producing diminishing but real returns&lt;/li&gt;
&lt;li&gt;Setup-based infrastructure + model-based creation: tested over 3 projects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What isn't working:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Existing file modification: consistently unreliable with our current model and configuration&lt;/li&gt;
&lt;li&gt;Non-deterministic results on complex tasks: one task passed in 2 out of 3 runs, failed in 1. Same code, same model, different outcome.&lt;/li&gt;
&lt;li&gt;Long dependency chains: a single DEADLOCK blocks everything downstream&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Open questions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does CL-071 hold on 20+ task projects with complex dependency graphs?&lt;/li&gt;
&lt;li&gt;Does the "new file only" constraint create unsustainable file fragmentation at scale?&lt;/li&gt;
&lt;li&gt;Will newer local models (Qwen3-Coder v2, Llama 4) shift this boundary?&lt;/li&gt;
&lt;li&gt;Is this specific to quantized local models, or do cloud API models show similar patterns on file modification tasks inside TDD loops?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A Request to Readers
&lt;/h2&gt;

&lt;p&gt;If you're running local models — Ollama, llama.cpp, vLLM, or something else — within autonomous execution loops, we'd be interested in learning whether your telemetry shows similar variations between file creation and file modification tasks.&lt;/p&gt;

&lt;p&gt;Specifically: &lt;strong&gt;how do your local configurations handle incremental diff generation inside structured loops versus generating complete, fresh modules from detailed specs?&lt;/strong&gt; If you've logged similar boundaries or found alternative designs to work around modification deadlocks, please share your setup and observations in the comments.&lt;/p&gt;

&lt;p&gt;We're also curious whether anyone has hard metrics on how cloud models (GPT, Claude) perform on targeted file modifications inside closed-loop TDD environments. Our dataset is one model family on one hardware tier — more data points from different setups would help everyone working in this space.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Project 13 will be the first real test of whether CL-071 is a design principle or just a project-12-specific workaround. Every implementation task will target a new file. Setup will handle all infrastructure. The open question isn't whether it passes — it's whether the "new file only" constraint produces a project structure that's actually maintainable at 20+ tasks.&lt;/p&gt;

&lt;p&gt;We're also adding automatic CL-071 validation to &lt;code&gt;validate_prd.py&lt;/code&gt; — a check that flags any task whose implementation target already exists at execution time. For our workflow, rules that repeatedly affect outcomes should probably be machine-enforced.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Series So Far
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/josephyeo/i-built-a-local-ai-coding-agent-on-m5-max-128gb-it-failed-164-times-before-passing-35-tests-2fgj"&gt;I Built a Local AI Coding Agent on M5 Max 128GB&lt;/a&gt; — 164 failures, 35 tests, proof of concept&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/josephyeo/we-didnt-migrate-from-n8n-to-python-because-n8n-failed-k9j"&gt;We Didn't Migrate from n8n to Python Because n8n Failed&lt;/a&gt; — The orchestrator rewrite&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/josephyeo/the-determinism-war-why-we-stopped-chasing-better-models-3c21"&gt;The Determinism War&lt;/a&gt; — Why we stopped chasing better models&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/josephyeo/the-information-design-gap-why-our-ai-agent-was-coding-blind-4p8o"&gt;The Information Design Gap&lt;/a&gt; — Why the agent was coding blind&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/josephyeo/dcr-wasnt-enough-why-ai-coding-agents-also-need-information-quality-1da4"&gt;DCR Wasn't Enough&lt;/a&gt; — Adding information quality to the framework&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/josephyeo/the-bug-wasnt-in-the-model-lessons-from-9-local-ai-coding-agent-projects-18aa"&gt;The Bug Wasn't in the Model&lt;/a&gt; — Lessons from 9 projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The File Modification Boundary&lt;/strong&gt; — You are here. 12 projects, a boundary mapped.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  About
&lt;/h2&gt;

&lt;p&gt;I'm Joseph YEO, a solo builder from Seoul, Korea. ForgeFlow runs entirely on a MacBook Pro M5 Max 128GB — no cloud APIs during execution. The planning agent (Claude) designs the specs. The local model (Qwen3-Coder-Next, 45GB Q4_K_M) executes the TDD loop autonomously.&lt;/p&gt;

&lt;p&gt;Follow along:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;𝕏: &lt;a href="https://x.com/josephyeo_dev" rel="noopener noreferrer"&gt;@josephyeo_dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/joseph-yeo" rel="noopener noreferrer"&gt;joseph-yeo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://projectjoseph.dev" rel="noopener noreferrer"&gt;projectjoseph.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built over 81 sessions, May 2026. All models run locally via Ollama 0.23.0 on macOS. No cloud APIs were used during autonomous execution.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was drafted with Claude and edited by me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The Bug Wasn't in the Model: Lessons from 9 Local AI Coding Agent Projects</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Sun, 17 May 2026 14:11:48 +0000</pubDate>
      <link>https://dev.to/josephyeo/the-bug-wasnt-in-the-model-lessons-from-9-local-ai-coding-agent-projects-18aa</link>
      <guid>https://dev.to/josephyeo/the-bug-wasnt-in-the-model-lessons-from-9-local-ai-coding-agent-projects-18aa</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 6 of the ForgeFlow series. &lt;a href="https://dev.to/josephyeo/dcr-wasnt-enough-why-ai-coding-agents-also-need-information-quality"&gt;Part 5: DCR Wasn't Enough&lt;/a&gt; introduced the two-axis model: DCR × Information Quality. We ended Part 5 at three projects and a 29% pass rate. Here's how we reached 100% on a controlled project — and why we needed a third axis.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Part 5 ended with a framework and a question.&lt;/p&gt;

&lt;p&gt;The framework said: &lt;em&gt;System Reliability ≈ DCR × Information Quality.&lt;/em&gt; The question was whether that would actually hold up as we kept running projects.&lt;/p&gt;

&lt;p&gt;We ran six more. Same model. Same hardware. No cloud APIs during execution. By project nine, the autonomous pass rate hit &lt;strong&gt;100% on that specific project&lt;/strong&gt; — eight tasks, thirty-one tests, four minutes, zero manual intervention.&lt;/p&gt;

&lt;p&gt;This post is about the path from 29% to 100%, and the third variable we didn't expect to find.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Scoreboard
&lt;/h2&gt;

&lt;p&gt;Here's the full longitudinal data. Nine projects, same 45GB local model, same hardware throughout:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Pass Rate&lt;/th&gt;
&lt;th&gt;CL Rules&lt;/th&gt;
&lt;th&gt;Key Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;repo-jwt&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;No design rules existed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;todo-api&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;~10&lt;/td&gt;
&lt;td&gt;Context files added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;bookmark-api&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;~20&lt;/td&gt;
&lt;td&gt;Full information pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;expense-tracker&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;New failure patterns emerged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;rating-api&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;DB fixture issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;library-api&lt;/td&gt;
&lt;td&gt;0% → scrapped&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;Fundamental architecture gap*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;event-api&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;Setup script pattern validated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;habit-tracker&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;Route tasks collapsed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;contact-book&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;All axes aligned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The 100% figure refers to Project 9 only, not the aggregate across all nine projects.&lt;/strong&gt; We used it as a controlled checkpoint: after fixing the route-task failure pattern from Project 8, could the same local model complete a comparable route-heavy project without intervention?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;*Project 6 was scrapped because it required an architectural paradigm change (multi-model foreign-key setup scripts) that our orchestrator couldn't state-track at the time. Rather than polluting the loop data with a mismatched setup, we halted execution to redesign our baseline infrastructure scripts. The lessons from that failure directly produced CL-036 through CL-039.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The trajectory isn't a clean line upward. Project 3 hit 100%, then projects 4 through 8 dropped back. Each drop exposed a new category of failure that our rules didn't cover yet.&lt;/p&gt;

&lt;p&gt;This is the pattern that mattered most to us: &lt;strong&gt;in these nine bounded projects, every failure we investigated had a concrete system-level fix. We did not find a case where replacing the model was the only plausible remedy.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Crystallization Loop
&lt;/h2&gt;

&lt;p&gt;Each project failure produced what we call a "Crystallized Lesson" (CL) — a concrete, testable rule that prevents that specific failure from recurring. Not a vague principle. A rule precise enough that code could check it.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CL-005:&lt;/strong&gt; Infrastructure files (conftest.py, database.py) must never appear in a task's target files. &lt;em&gt;Origin: Project 3, where the model kept overwriting shared fixtures.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CL-034:&lt;/strong&gt; DateTime fields with SQLAlchemy's &lt;code&gt;default=&lt;/code&gt; must be set in Python's &lt;code&gt;__init__&lt;/code&gt;, not relied upon at DB insert time. &lt;em&gt;Origin: Project 5, where unit tests failed because &lt;code&gt;created_at&lt;/code&gt; was None before flush.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CL-043:&lt;/strong&gt; When adding an endpoint to an existing route file, each task must contain exactly one endpoint. &lt;em&gt;Origin: Project 8, where multi-endpoint tasks caused the model to time out trying to understand the existing code.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By project 9, we had 43 of these rules. They're not guidelines — they're checkable constraints on the PRD document that feeds the model. We call the document that holds them the &lt;em&gt;PRD Design Checklist&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here's what the accumulation looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Projects 1-3:  CL-001 to CL-020  (~7 per project)
Projects 4-6:  CL-021 to CL-035  (~5 per project)
Projects 7-8:  CL-036 to CL-043  (~4 per project)
Project 9:     0 new CLs needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rate of new rules slowed, but the &lt;em&gt;depth&lt;/em&gt; increased. Early rules were about file placement ("where does conftest.py go?"). Later rules were about engine-level behavior ("how does the correction system handle idempotency?").&lt;/p&gt;




&lt;h2&gt;
  
  
  What Project 8 Broke
&lt;/h2&gt;

&lt;p&gt;Project 8 (habit-tracker-api) is worth examining because it's where the two-axis model from Part 5 stopped being sufficient.&lt;/p&gt;

&lt;p&gt;The project had nine tasks. The first four — model and schema creation — passed autonomously in one cycle each. Then the route tasks (5 through 9) collapsed. Zero of five passed.&lt;/p&gt;

&lt;p&gt;The failures fell into four categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A pytest configuration warning was being captured as a failure signature.&lt;/strong&gt; The code was correct, but the orchestrator classified it as broken.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A string-replacement correction was applied twice.&lt;/strong&gt; &lt;code&gt;client.post(&lt;/code&gt; → &lt;code&gt;await client.post(&lt;/code&gt; was also applied to lines that already had &lt;code&gt;await&lt;/code&gt;, producing &lt;code&gt;await await client.post(&lt;/code&gt; — a syntax error.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A schema class was never generated&lt;/strong&gt; because no test existed for it. The model only builds what it's tested for. No test, no code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tasks that modified existing files timed out&lt;/strong&gt; because the model needed too long to understand the accumulated code.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice: none of these are IQ problems in the Part 4/5 sense. The model had all the information it needed. The PRD was well-designed by the standards we had at the time. The failures came from &lt;strong&gt;the engine itself&lt;/strong&gt; — the orchestrator's correction logic, its gate system, its timeout handling.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Third Variable: Engine Quality
&lt;/h2&gt;

&lt;p&gt;This forced us to extend the two-axis model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I don't mean this as a measured mathematical product yet, but as a diagnostic model: if any axis collapses, the whole loop collapses.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or to put it less formally: you can have a perfectly designed PRD and a well-informed model, and still fail because the orchestrator has bugs.&lt;/p&gt;

&lt;p&gt;By engine quality, we mean whether the orchestrator preserves the intended semantics of the execution loop: phase isolation (RED writes only tests, GREEN writes only implementation), retry correctness (rollbacks don't destroy infrastructure state), deterministic correction safety (rewrites don't corrupt already-correct code), timeout policy, and commit boundaries.&lt;/p&gt;

&lt;p&gt;In our case, the concrete fixes were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The correction engine's idempotency.&lt;/strong&gt; Our string-replacement system applied corrections blindly, turning &lt;code&gt;client.post(&lt;/code&gt; into &lt;code&gt;await await client.post(&lt;/code&gt;. The fix was a line-level guard: if the replacement text already exists on a line, skip it. (We're aware this is a limitation of primitive string matching. A proper AST-based mutation engine using something like LibCST would eliminate this entire class of errors. That's on our roadmap but hasn't been necessary yet at our current project complexity.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The RED phase scope.&lt;/strong&gt; If the model outputs an implementation file during the test-writing phase and the orchestrator writes it to disk, the test passes immediately — and the TDD cycle breaks. The fix was restricting the RED phase's file-write scope to test files only.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Router registration resilience.&lt;/strong&gt; If &lt;code&gt;git reset --hard&lt;/code&gt; during a retry also reverts infrastructure changes made by the orchestrator's auto-registration system, the next cycle starts with a broken setup. The fix was committing router registration during the initial setup script, not inside the TDD cycle.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We fixed these with three targeted engine patches, each with its own test suite (16 tests total). After the fixes, project 9 ran eight tasks with zero failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key insight for us:&lt;/strong&gt; PRD quality and engine quality appeared to be independent variables. Improving one didn't fix the other. Project 8's 44% pass rate wasn't a PRD problem — it was an engine problem that looked like a PRD problem until we traced each failure to its root cause.&lt;/p&gt;




&lt;h2&gt;
  
  
  What 100% Actually Looked Like
&lt;/h2&gt;

&lt;p&gt;Project 9 was a contact book API with search. Single model (no foreign keys), six CRUD endpoints plus a search-by-query-param feature. We chose it deliberately to test route-task decomposition — the exact pattern that failed in project 8.&lt;/p&gt;

&lt;p&gt;The numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tasks&lt;/td&gt;
&lt;td&gt;8 (model, schemas, 6 endpoints)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total cycles&lt;/td&gt;
&lt;td&gt;8 (every task passed first try)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total tokens&lt;/td&gt;
&lt;td&gt;9,042 (Ollama-reported generated tokens; prompts excluded)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total time&lt;/td&gt;
&lt;td&gt;~4 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests generated&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual intervention&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud API cost&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each task followed the same loop: Ollama writes a failing test → Ollama writes minimum implementation → deterministic corrections applied → pytest runs → commit if green.&lt;/p&gt;

&lt;p&gt;The route tasks that had failed repeatedly in project 8 now passed in single cycles. The differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each task added exactly one endpoint (CL-043)&lt;/li&gt;
&lt;li&gt;All schema classes had test coverage (CL-042)&lt;/li&gt;
&lt;li&gt;The asyncio configuration was pre-set in the setup script (CL-040)&lt;/li&gt;
&lt;li&gt;Trailing-slash corrections were applied deterministically (new)&lt;/li&gt;
&lt;li&gt;Router registration was committed during setup, not during the TDD cycle (new)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these changes required a better model. The model was the same 45GB Qwen3 that produced 0% on project 1.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why 100% Doesn't Mean "Solved"
&lt;/h2&gt;

&lt;p&gt;I want to be careful here.&lt;/p&gt;

&lt;p&gt;100% on a contact book API doesn't mean ForgeFlow can build anything. A contact book API is an architectural sandbox. The project was deliberately chosen to isolate the route-task failure pattern. It had no foreign keys, no authentication, no file uploads. Each endpoint was independent. The success here suggests that our execution loop is stable under these constrained conditions, not that it can refactor a legacy microservice architecture.&lt;/p&gt;

&lt;p&gt;The real test is whether the next project — something with two related models and foreign-key relationships — maintains a high pass rate. We don't know yet.&lt;/p&gt;

&lt;p&gt;What we do know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The crystallization loop works.&lt;/strong&gt; Each failure produces a rule. Rules accumulate. The same failure hasn't recurred in subsequent comparable projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine fixes matter as much as PRD fixes.&lt;/strong&gt; Three engine patches in one session unblocked a project that no amount of PRD improvement would have fixed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The three-variable model explains our data better than two.&lt;/strong&gt; Projects 4-8 had good PRDs but engine bugs. The two-axis model couldn't explain those drops. The three-variable model can.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Failure Catalog
&lt;/h2&gt;

&lt;p&gt;Across nine projects, we cataloged 19 distinct failure patterns. Every one was eventually addressed — either through a PRD design rule, an engine fix, or a setup script change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;Resolution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PRD design gap&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;CL rules in checklist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine bug&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;forgeflow.py patches + tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure/setup&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Setup script standardization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout/performance&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;aider_timeout configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few examples from the catalog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FC-015: Non-idempotent correction rule.&lt;/strong&gt; Symptom: deterministic correction produced &lt;code&gt;await await client.post(...)&lt;/code&gt;. Root cause: the string-replacement rule did not check whether the line was already corrected. Fix: line-level idempotency guard — if the replacement text already exists on a given line, skip the replacement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FC-018: RED phase implementation leakage.&lt;/strong&gt; Symptom: tests passed immediately during RED because the implementation file was also written to disk. Root cause: the orchestrator's file-write scope included both test and implementation files during the test-writing phase. Fix: restrict RED phase scope to test files only; reject or quarantine non-test files during RED.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FC-019a: Router registration lost across retry.&lt;/strong&gt; Symptom: every retry started from a broken app state (404 on all endpoints) after &lt;code&gt;git reset --hard&lt;/code&gt;. Root cause: the orchestrator's auto-registration system added router imports to &lt;code&gt;main.py&lt;/code&gt; during the TDD cycle, but &lt;code&gt;git reset&lt;/code&gt; reverted those changes. Fix: commit router registration during the initial setup script, before the TDD loop begins.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under our postmortem classification criteria, none of the 19 cataloged failures were classified as pure model-capability failures — cases where the model lacked the syntax or logic ability to solve the task. Every failure traced back to something in the system around the model: missing information, incorrect scaffolding, or engine bugs.&lt;/p&gt;

&lt;p&gt;This doesn't mean model capability doesn't matter. A stronger model would probably tolerate worse PRDs and buggier engines. But in our limited experience, fixing the system was always cheaper and more permanent than hoping for a smarter model.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;We're designing a diagnostic pipeline that applies the failure catalog automatically. The idea: when a task deadlocks, the engine checks the failure catalog for a matching pattern before giving up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[DEADLOCK DETECTED]
        │
        ▼
[Pattern Match: Failure Catalog]
        ├──► Match Found ──► Apply Fix (Deterministic) ──► Retry
        └──► No Match   ──► Local LLM Diagnosis (Stage 2)
                                   └──► Fails ──► Human Escalation (Stage 3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stage 1 is pure pattern matching — deterministic, no LLM needed. Stage 2 would use the local model to diagnose novel failures. Stage 3 remains human review.&lt;/p&gt;

&lt;p&gt;The goal isn't to eliminate human involvement entirely. It's to ensure that &lt;strong&gt;each human intervention produces a rule that prevents the same intervention next time.&lt;/strong&gt; The system should get cheaper to operate with every project it runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Thesis, Updated
&lt;/h2&gt;

&lt;p&gt;Part 3: &lt;em&gt;"The bottleneck is not model capability, but the verifiability of specifications."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Part 4: &lt;em&gt;"Even after verifiability is constructed, the bottleneck shifts to information delivery."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Part 5: &lt;em&gt;"An AI coding agent's reliability is a product of its deterministic coverage and its information quality."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now, the working version after nine projects:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;"In our experience, an AI coding agent's reliability is bounded by three independent variables: the determinism of its scaffolding, the quality of information it receives, and the correctness of its own engine. Improving any two without the third produced a system that failed in ways that looked like model limitations but weren't."&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The practical diagnostic is now threefold: measure your deterministic coverage, inspect your information quality, and test the engine itself. Fix the axis that's actually broken. In our nine projects, that diagnostic kept pointing to the system, not the model.&lt;/p&gt;

&lt;p&gt;Whether that pattern holds at higher complexity is something we're still finding out.&lt;/p&gt;




&lt;h2&gt;
  
  
  About
&lt;/h2&gt;

&lt;p&gt;I'm Joseph YEO, building ForgeFlow from Seoul, Korea — a local AI coding agent that runs entirely on Apple Silicon, no cloud inference during execution.&lt;/p&gt;

&lt;p&gt;What's your experience with orchestrator-level bugs masquerading as model limitations? Have you seen cases where the system around the model was the actual bottleneck? I'd love to compare notes.&lt;/p&gt;

&lt;p&gt;Follow along:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;𝕏: &lt;a href="https://x.com/josephyeo_dev" rel="noopener noreferrer"&gt;@josephyeo_dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/joseph-yeo" rel="noopener noreferrer"&gt;joseph-yeo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://projectjoseph.dev/" rel="noopener noreferrer"&gt;projectjoseph.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Previous parts: &lt;a href="https://dev.to/josephyeo/i-built-a-local-ai-coding-agent-on-m5-max-128gb-it-failed-164-times-before-passing-35-tests-2fgj"&gt;Part 1: 164 Failures&lt;/a&gt; · &lt;a href="https://dev.to/josephyeo/we-didnt-migrate-from-n8n-to-python-because-n8n-failed-k9j"&gt;Part 2: n8n to Python&lt;/a&gt; · &lt;a href="https://dev.to/josephyeo/the-determinism-war-why-we-stopped-chasing-better-models-3c21"&gt;Part 3: The Determinism War&lt;/a&gt; · &lt;a href="https://dev.to/josephyeo/the-information-design-gap-why-our-ai-agent-was-coding-blind-4p8o"&gt;Part 4: The Information Design Gap&lt;/a&gt; · Part 5: DCR Wasn't Enough&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;9 projects. 43 rules. 19 failure patterns. 48 development sessions. Same 45GB model throughout. All models run locally via Ollama 0.23.0 on Apple Silicon M5 Max 128GB. No cloud APIs were used during autonomous execution.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was drafted with Claude and edited by me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>programming</category>
    </item>
    <item>
      <title>DCR Wasn't Enough: Why AI Coding Agents Also Need Information Quality</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Thu, 14 May 2026 14:02:17 +0000</pubDate>
      <link>https://dev.to/josephyeo/dcr-wasnt-enough-why-ai-coding-agents-also-need-information-quality-1da4</link>
      <guid>https://dev.to/josephyeo/dcr-wasnt-enough-why-ai-coding-agents-also-need-information-quality-1da4</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 5 of the ForgeFlow series. &lt;a href="https://dev.to/josephyeo/the-determinism-war-why-we-stopped-chasing-better-models-3c21"&gt;Part 3: The Determinism War&lt;/a&gt; introduced DCR. &lt;a href="https://dev.to/josephyeo/the-information-design-gap-why-our-ai-agent-was-coding-blind-4p8o"&gt;Part 4: The Information Design Gap&lt;/a&gt; showed how information delivery moved our pass rate from 0% to 67%.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;We thought we had the answer.&lt;/p&gt;

&lt;p&gt;In Part 4, we showed that fixing our information pipeline — zero code changes, just better PRD design — moved ForgeFlow's autonomous pass rate from 0% to 67%. We thought the lesson was clear: give the model enough context and it delivers.&lt;/p&gt;

&lt;p&gt;Then we ran a third project. Same model. Same engine. Same careful PRD design. The pass rate dropped to &lt;strong&gt;29%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This post is about why that happened and what it taught us about measuring AI coding agents.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Went Wrong on Project C
&lt;/h2&gt;

&lt;p&gt;Project C was a bookmark API with many-to-many relationships — more complex than a simple CRUD, but not wildly different in structure. We applied everything we learned from Project B: explicit context files per task, detailed descriptions, proper test scenario format.&lt;/p&gt;

&lt;p&gt;The PRD had sixteen tasks. In the first seven executed tasks, only two passed autonomously — &lt;strong&gt;2 / 7, or 29%&lt;/strong&gt;. The other five required manual intervention.&lt;/p&gt;

&lt;p&gt;The failures weren't the same as Project A's. In Project A, the model hallucinated imports and invented fixtures because it couldn't see the project. In Project C, it could see the project — but it kept hitting &lt;strong&gt;runtime patterns our prompt pipeline had not exposed clearly enough:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pydantic's &lt;code&gt;HttpUrl&lt;/code&gt; adds a trailing slash that breaks equality checks&lt;/li&gt;
&lt;li&gt;FastAPI's router prefix with &lt;code&gt;"/"&lt;/code&gt; vs &lt;code&gt;""&lt;/code&gt; causes 307 redirects&lt;/li&gt;
&lt;li&gt;SQLAlchemy async many-to-many relationships trigger &lt;code&gt;MissingGreenlet&lt;/code&gt; errors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;create_async_engine&lt;/code&gt; lives in &lt;code&gt;sqlalchemy.ext.asyncio&lt;/code&gt;, not &lt;code&gt;sqlalchemy&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would not classify these primarily as intelligence failures. They looked more like &lt;strong&gt;behavioral knowledge gaps&lt;/strong&gt; — framework-specific quirks that context files alone didn't expose.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Context Files Weren't Enough
&lt;/h2&gt;

&lt;p&gt;This forced us to confront a limitation in our Part 4 approach.&lt;/p&gt;

&lt;p&gt;Context files are &lt;strong&gt;static&lt;/strong&gt;. They're defined when you write the PRD — before any code exists. By TASK-007, the project has files that weren't there when the PRD was written. The model can't see them unless someone manually updates the list.&lt;/p&gt;

&lt;p&gt;For example, TASK-007 needed to create a route that depended on models and schemas generated by TASK-003 and TASK-005. But those files didn't exist when the PRD was written. The context file list was correct at design time — and stale by execution time.&lt;/p&gt;

&lt;p&gt;And even when context files are current, they show the model &lt;em&gt;what code exists&lt;/em&gt;. They don't teach the model &lt;em&gt;how the runtime behaves&lt;/em&gt;. No amount of reading &lt;code&gt;bookmark.py&lt;/code&gt; will tell you that &lt;code&gt;bookmark.tags.append(tag)&lt;/code&gt; triggers a synchronous database call inside an async context.&lt;/p&gt;

&lt;p&gt;We realized we needed two different kinds of information, not one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it provides&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Structural information&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What files exist, what they export, how they relate&lt;/td&gt;
&lt;td&gt;Context files, repo maps&lt;/td&gt;
&lt;td&gt;"BookmarkCreate has a field &lt;code&gt;url: HttpUrl&lt;/code&gt;"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Behavioral knowledge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;How the runtime actually works, framework quirks, patterns to avoid&lt;/td&gt;
&lt;td&gt;Accumulated experience, failure analysis&lt;/td&gt;
&lt;td&gt;"HttpUrl adds a trailing slash — use &lt;code&gt;str(url).rstrip('/')&lt;/code&gt; for comparison"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Project B's success came from fixing structural information. Project C's failures seemed to come largely from missing behavioral knowledge. Both feed into what the model needs, but they come from different places and accumulate differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Axes, Not One
&lt;/h2&gt;

&lt;p&gt;This is what led us to think about AI coding agent performance along two axes instead of one.&lt;/p&gt;

&lt;p&gt;After Part 3, we had &lt;strong&gt;DCR&lt;/strong&gt; — the ratio of decisions handled deterministically. At 85%, the model's job was narrow: just write the code.&lt;/p&gt;

&lt;p&gt;After Part 4, we had the &lt;strong&gt;Information Design Gap&lt;/strong&gt; — the model needs enough context to do that narrow job.&lt;/p&gt;

&lt;p&gt;Now, after three projects, we're working with a slightly more structured version:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Axis 1: DCR (Deterministic Coverage Ratio)&lt;/strong&gt; — how much of the decision surface is handled without the model. This is the scaffolding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Axis 2: Information Quality (IQ)&lt;/strong&gt; — how well the model is equipped for the decisions it &lt;em&gt;does&lt;/em&gt; handle. This is the fuel.&lt;/p&gt;

&lt;p&gt;This is not a measured equation — just the mental model that best explains our runs so far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System Reliability ≈ DCR × Information Quality
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DCR narrows the blast radius. IQ determines how well the model performs inside it. In our data so far, you need both. Neither alone has been sufficient.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Dimensions of Information Quality
&lt;/h2&gt;

&lt;p&gt;After analyzing our failures across three projects and reading through recent work on LLM-based code generation, we've found that "the model didn't get enough information" breaks down into at least three problems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dimension 1 — Availability: Does the information exist in the context window?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was Project A's problem. The model received ~240 tokens of task-relevant content. The information existed on disk — the orchestrator just never loaded it.&lt;/p&gt;

&lt;p&gt;Dillon &amp;amp; Varanasi (2026) observed something similar. They measured whether generated code follows team-level architectural decisions. When a decision was visible in the files the model received, compliance was near-perfect. When it existed only in documents the model never saw, compliance dropped to zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dimension 2 — Selection: Is irrelevant information excluded?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;More context isn't always better. Alonso et al. (2026) found that adding procedural TDD instructions increased regressions, while a targeted test map reduced them significantly. The practical lesson for us was simple: token budget is finite.&lt;/p&gt;

&lt;p&gt;Hu et al. (2026) quantified this from the other direction. In their cross-file code generation benchmark, 62% of functions didn't need cross-file context at all. The skill is knowing &lt;em&gt;which&lt;/em&gt; information to include.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dimension 3 — Structure: Is the information formatted for the model to use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the counterintuitive one. Information can be &lt;em&gt;available&lt;/em&gt; and &lt;em&gt;selected correctly&lt;/em&gt; but still fail because of how it's structured.&lt;/p&gt;

&lt;p&gt;Hu et al.'s ablation showed this clearly. "Inlined" context (dependencies inserted at relevant code locations) versus "prepended" context (same information at the top of the prompt) — same information, different structure. Removing the inlining degraded performance to nearly the same level as removing the context entirely.&lt;/p&gt;

&lt;p&gt;Chinthareddy (2026) found a similar pattern with code retrieval. On a set of architectural queries, a deterministic AST-derived knowledge graph scored 100% correctness while a vector-similarity approach on the same codebase scored 40% (on the Shopizer benchmark suite). The gap came from how relationships were structured, not what information was available.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Two Axes Interact
&lt;/h2&gt;

&lt;p&gt;Here's why we think you need both:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;DCR&lt;/th&gt;
&lt;th&gt;IQ&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;System has a chance to work. Deterministic decisions are correct, model has what it needs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Deterministic decisions are correct, but the model is flying blind in its narrow lane.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Model generates good code, but the system mishandles it — wrong task order, broken gates, environment failures.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Failures become hard to diagnose. This may be what "the model isn't smart enough" often looks like.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Our Project A was Scenario B. DCR was 85% — the harness was solid. But Information Quality was ~15%. The model couldn't do its job because it couldn't see the project.&lt;/p&gt;

&lt;p&gt;Project B was closer to Scenario A. Same DCR. At least on the availability dimension, the delivered context moved closer to ~80%. The model had enough context to complete most of the tasks that fit the orchestration loop.&lt;/p&gt;

&lt;p&gt;Project C showed us that IQ itself has layers. Structural availability was good (~80%), but behavioral knowledge was missing. The two-axis model held — DCR was fine, IQ was the bottleneck — but the &lt;em&gt;nature&lt;/em&gt; of the IQ problem was different from Project A.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Practical Diagnostic
&lt;/h2&gt;

&lt;p&gt;If you're building or evaluating an AI coding agent, here's the check we now run on our own system:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Measure your DCR.&lt;/strong&gt; List every decision point in one execution cycle. For each one, ask: is this resolved by deterministic code, or does it depend on model output? Count the ratio. If it's below 50%, the scaffolding likely needs reinforcement before the model can succeed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Dump the prompt.&lt;/strong&gt; Not the template — the actual string that reaches the model at inference time. Read it as if you're a developer seeing this codebase for the first time. Can you write the code from this prompt alone? If you can't, the model can't either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Diagnose by axis.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High DCR, low pass rate → Information Quality problem. Check: are context files loaded? Are descriptions specific enough? Are test assertions reaching the model?&lt;/li&gt;
&lt;li&gt;Low DCR, inconsistent results → Structural problem. The model is making decisions that should be deterministic. Move those decisions into code.&lt;/li&gt;
&lt;li&gt;Both seem fine, still failing → Might be a genuine model capability limit. Only after that does a model upgrade become the next reasonable hypothesis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We jumped to Step 3 after Project A. "Qwen3 can't handle JWT auth" was our first diagnosis. Our initial diagnosis was premature. The bigger problem was that the information pipeline was effectively empty. We could have saved ourselves weeks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related Work Pointing in a Similar Direction
&lt;/h2&gt;

&lt;p&gt;I didn't set out to build a framework. I set out to figure out why Project A failed. But a consistent pattern kept showing up in recent work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2603.17973" rel="noopener noreferrer"&gt;Alonso et al. (2026)&lt;/a&gt;&lt;/strong&gt; — TDD procedure instructions hurt. Contextual test maps helped. Procedure without context was counterproductive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2601.13118" rel="noopener noreferrer"&gt;Midolo et al. (2026)&lt;/a&gt;&lt;/strong&gt; — Surveyed 50 developers. 14% independently reported "contextual information about other system components" as a missing factor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2511.12823" rel="noopener noreferrer"&gt;Jalil et al. (2025)&lt;/a&gt;&lt;/strong&gt; — Smaller models with TDD and code execution surpassed larger models without those supports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2605.08112" rel="noopener noreferrer"&gt;Dillon &amp;amp; Varanasi (2026)&lt;/a&gt;&lt;/strong&gt; — Decision compliance went from 46% to 95% by adding product context and structured specs. Cost per merge-ready task dropped 68%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2601.00376" rel="noopener noreferrer"&gt;Hu et al. (2026)&lt;/a&gt;&lt;/strong&gt; — Cross-file inlining improved exact match by a reported average of 29.73% on RepoExec across three backbone models. The result was model-independent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2601.08773" rel="noopener noreferrer"&gt;Chinthareddy (2026)&lt;/a&gt;&lt;/strong&gt; — Deterministic AST-derived code graphs achieved 100% correctness vs. 40% for vector-only retrieval on architectural queries (Shopizer suite). LLM-based graph extraction missed 31% of files entirely.&lt;/p&gt;

&lt;p&gt;These studies don't prove our framework. But they point in a consistent direction, and our three internal runs are consistent with that direction.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We're Not Claiming
&lt;/h2&gt;

&lt;p&gt;I want to be precise about the boundaries here.&lt;/p&gt;

&lt;p&gt;We're not claiming that model capability doesn't matter. It does — for the non-deterministic slice. A stronger model will generate better code from the same information.&lt;/p&gt;

&lt;p&gt;We're not claiming these two metrics capture everything. Latency, cost, context window size, tool use ability — all matter. But in our limited experience, DCR and IQ have explained the largest share of variance in autonomous pass rates.&lt;/p&gt;

&lt;p&gt;We're also not claiming this is proven. ForgeFlow is a sample size of one. We have three data points (0%, 67%, 29%) and they're consistent with the two-axis model, but three points don't make a proof.&lt;/p&gt;

&lt;p&gt;If anyone has run similar experiments — different scaffolding levels, different context strategies, measured pass rates — I'd genuinely love to compare notes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Thesis, So Far
&lt;/h2&gt;

&lt;p&gt;Part 3: &lt;em&gt;"The bottleneck is not model capability, but the verifiability of specifications."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Part 4: &lt;em&gt;"Even after verifiability is constructed, the bottleneck shifts to information delivery."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now the version we're working with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;"An AI coding agent's reliability seems to be a product of its deterministic coverage and its information quality. Improving either without the other produces a system that is either structurally sound but informationally blind, or well-informed but structurally fragile."&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two axes. One product. Neither alone has been sufficient in our experience.&lt;/p&gt;

&lt;p&gt;Measure your DCR. Dump your prompt. Fix the axis that's actually broken. Only after that does a model upgrade become the next reasonable hypothesis. That's the diagnostic that's worked for us. Whether it generalizes is something we're still finding out.&lt;/p&gt;




&lt;h2&gt;
  
  
  About
&lt;/h2&gt;

&lt;p&gt;I'm Joseph YEO, building ForgeFlow from Seoul, Korea — a local AI coding agent that runs entirely on Apple Silicon, no cloud inference during execution.&lt;/p&gt;

&lt;p&gt;This post synthesizes what I've learned from running three projects end-to-end and reading 40+ papers on LLM-based code generation. The two-axis model isn't a proven theory — it's the working diagnostic I use every time a cycle fails. I'm sharing it because it's been useful, and because I'm curious whether others are seeing the same patterns.&lt;/p&gt;

&lt;p&gt;How are you handling the "stale context" problem as your agent modifies the codebase? Are you using repo maps, re-indexing on every task, or something else entirely? I'd love to hear what's working.&lt;/p&gt;

&lt;p&gt;Follow along:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;𝕏: &lt;a href="https://x.com/josephyeo_dev" rel="noopener noreferrer"&gt;@josephyeo_dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/joseph-yeo" rel="noopener noreferrer"&gt;joseph-yeo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://projectjoseph.dev/" rel="noopener noreferrer"&gt;projectjoseph.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;All models run locally via Ollama 0.23.0 on Apple Silicon M5 Max 128GB. No cloud APIs were used during autonomous execution.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was drafted with Claude and edited by me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The Information Design Gap: Why Our AI Agent Was Coding Blind</title>
      <dc:creator>Joseph Yeo</dc:creator>
      <pubDate>Wed, 13 May 2026 15:23:44 +0000</pubDate>
      <link>https://dev.to/josephyeo/the-information-design-gap-why-our-ai-agent-was-coding-blind-4p8o</link>
      <guid>https://dev.to/josephyeo/the-information-design-gap-why-our-ai-agent-was-coding-blind-4p8o</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 4 of the ForgeFlow series. &lt;a href="https://dev.to/josephyeo/the-determinism-war-why-we-stopped-chasing-better-models-3c21"&gt;Part 3: The Determinism War&lt;/a&gt; introduced DCR (Deterministic Coverage Ratio) and why we stopped chasing better models.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In Part 3, I proposed a hypothesis:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The bottleneck of LLM-driven software engineering is not model capability, but the verifiability of specifications."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then I said: "We're building the system to test it."&lt;/p&gt;

&lt;p&gt;We ran two projects. Same model. Same engine. Same orchestrator. The autonomous pass rate went from &lt;strong&gt;0% to 67%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In our case, the fix wasn't a better model. It was giving the model enough information to do its job.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Projects, One Model, One Engine
&lt;/h2&gt;

&lt;p&gt;ForgeFlow is a TDD orchestrator that runs entirely locally. No cloud API calls during execution. The cycle is simple: generate test (RED) → generate implementation (GREEN) → run pytest → commit or retry.&lt;/p&gt;

&lt;p&gt;We ran it against two internal projects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Project A: repo-jwt&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Project B: todo-api&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Domain&lt;/td&gt;
&lt;td&gt;JWT authentication API&lt;/td&gt;
&lt;td&gt;Todo CRUD API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tasks&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Qwen3-Coder-Next Q4_K_M (45GB)&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine&lt;/td&gt;
&lt;td&gt;forgeflow.py v2&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autonomous passes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 / 18 (0%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8 / 12 (67%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual intervention&lt;/td&gt;
&lt;td&gt;18 tasks (100%)&lt;/td&gt;
&lt;td&gt;4 tasks (33%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same model. Same engine. The pass rate changed from 0% to 67%.&lt;/p&gt;

&lt;p&gt;What changed between Project A and Project B was not the model or the orchestrator. It was the &lt;strong&gt;information structure of the PRD&lt;/strong&gt; — the spec document that tells the model what to build.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Prompt Dump That Exposed the Problem
&lt;/h2&gt;

&lt;p&gt;After Project A failed across all 18 tasks, we did something we should have done much earlier: we dumped the actual prompt the model received at inference time.&lt;/p&gt;

&lt;p&gt;Not the prompt template. Not the system prompt spec. The literal string that arrived at the model's context window.&lt;/p&gt;

&lt;p&gt;Here's what we expected to find: a rich prompt containing the task spec, relevant source files, test fixtures, data model definitions, and dependency context.&lt;/p&gt;

&lt;p&gt;What we actually found was a prompt of about &lt;strong&gt;~720 tokens&lt;/strong&gt; — and only &lt;strong&gt;~240 of those were task-relevant project information.&lt;/strong&gt; The rest was role text, formatting rules, and boilerplate.&lt;/p&gt;

&lt;p&gt;No source code. No test fixtures. No existing implementation files. The model was being asked to generate code for a project it could barely see.&lt;/p&gt;

&lt;p&gt;In hindsight, this was an embarrassing oversight. The information pipeline existed in the code — it just wasn't wired up. But we didn't notice until we read the raw prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Five Gaps
&lt;/h2&gt;

&lt;p&gt;We listed out every piece of information the model &lt;em&gt;should&lt;/em&gt; have received but didn't. Five gaps emerged:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 1 — No context in RED phase.&lt;/strong&gt; During test generation, the context parameter was hardcoded to an empty dictionary. The model wrote tests for modules it couldn't see, importing functions that didn't exist yet, guessing at fixture structures it had no way to know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 2 — No context file list in the PRD.&lt;/strong&gt; The orchestrator had a function ready to read context files from a task-level field. But the PRD never defined that field. So the function returned an empty list every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 3 — Module names without signatures.&lt;/strong&gt; The prompt listed available modules by name: &lt;code&gt;todo.py&lt;/code&gt;, &lt;code&gt;database.py&lt;/code&gt;. But not their contents, not their function signatures, not their class fields. The model knew &lt;em&gt;that&lt;/em&gt; modules existed, but not &lt;em&gt;what&lt;/em&gt; they contained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 4 — Test assertions not forwarded.&lt;/strong&gt; The PRD included test assertion fields with precise expected behavior. The prompt builder read a different field name. The assertions existed in the spec but never reached the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 5 — No conftest.py.&lt;/strong&gt; In a pytest project, &lt;code&gt;conftest.py&lt;/code&gt; defines shared fixtures — test database sessions, HTTP clients, factory functions. The model never saw it. Every task that required a test client, the model invented its own from scratch, often incompatibly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quantifying the Gap
&lt;/h2&gt;

&lt;p&gt;We measured how much relevant information actually reached the model by counting task-specific tokens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Task-relevant tokens&lt;/th&gt;
&lt;th&gt;What it contains&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What the model received&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~240&lt;/td&gt;
&lt;td&gt;Task ID, one-line description, module names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What a human developer would reference&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1,640&lt;/td&gt;
&lt;td&gt;Above + conftest.py, database.py, models, schemas, existing routes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model was operating on roughly &lt;strong&gt;15% of the information&lt;/strong&gt; a developer would use for the same task.&lt;/p&gt;

&lt;p&gt;We started calling this the &lt;strong&gt;Information Design Gap&lt;/strong&gt;: the difference between what a model &lt;em&gt;could&lt;/em&gt; use and what the system &lt;em&gt;actually delivers&lt;/em&gt; at inference time. Whether this framing is useful beyond our system is something we're still figuring out — but for us, it immediately clarified what to fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: No Code Changes
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprised us.&lt;/p&gt;

&lt;p&gt;The orchestrator already had the machinery to deliver context files. A function to resolve which files a task needs — existed. A function to read those files from disk — existed. A function to format them into the prompt — existed. The prompt builder had a slot for context.&lt;/p&gt;

&lt;p&gt;The pipeline existed. The PRD just wasn't feeding it.&lt;/p&gt;

&lt;p&gt;For Project B (todo-api), we made three changes — all in the PRD, none in the engine:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Added a context file list to every task.&lt;/strong&gt; Each task now listed exactly which existing files the model should see. Early tasks had empty lists. CRUD endpoint tasks included the test fixture file, the relevant model file, and the schema file.&lt;/p&gt;

&lt;p&gt;Here's what a task spec looked like after the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TASK-007&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;Create POST /api/todos.&lt;/span&gt;
    &lt;span class="s"&gt;Use the client fixture from conftest.py.&lt;/span&gt;
    &lt;span class="s"&gt;Return 201 with TodoResponse schema.&lt;/span&gt;
  &lt;span class="na"&gt;context_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tests/conftest.py&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app/models/todo.py&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app/schemas/todo.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Made descriptions explicit.&lt;/strong&gt; Instead of "Create the POST endpoint", we wrote "Create POST /api/todos. Use the &lt;code&gt;client&lt;/code&gt; fixture from conftest.py. Return 201 with TodoResponse schema." One sentence, but it told the model &lt;em&gt;where&lt;/em&gt; to look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Unified test scenario format.&lt;/strong&gt; Aligned PRD field names with what the prompt builder actually read, so test assertions reached the model.&lt;/p&gt;

&lt;p&gt;Total lines of code changed in the engine: &lt;strong&gt;zero.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What 67% Actually Means
&lt;/h2&gt;

&lt;p&gt;Eight of twelve tasks passed autonomously — the model generated code, tests passed, and ForgeFlow committed without human intervention for those tasks.&lt;/p&gt;

&lt;p&gt;Based on manual inspection, I would not classify the four manual tasks as model-capability failures. They were structural mismatches with the TDD cycle:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Why manual&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TASK-001&lt;/td&gt;
&lt;td&gt;Infrastructure setup — test and implementation must be created together&lt;/td&gt;
&lt;td&gt;RED phase incompatible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TASK-003&lt;/td&gt;
&lt;td&gt;Fixture-only — conftest.py defines fixtures, nothing to "fail" in RED&lt;/td&gt;
&lt;td&gt;No failing test possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TASK-010&lt;/td&gt;
&lt;td&gt;Validation already handled by Pydantic schema from an earlier task&lt;/td&gt;
&lt;td&gt;RED unexpected pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TASK-012&lt;/td&gt;
&lt;td&gt;Integration test — no implementation file, test-only task&lt;/td&gt;
&lt;td&gt;Engine assumes impl file exists&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That said, I should be honest about what we can't fully measure here. "Model-capability failure" is hard to distinguish from "subtle information gap we didn't notice." Our classification is based on manual inspection, not a controlled experiment. What we can say with confidence is that the &lt;em&gt;type&lt;/em&gt; of failure changed completely — from hallucinated imports and invented fixtures in Project A to structural mismatches in Project B.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lesson: Intelligence Gap vs. Information Gap
&lt;/h2&gt;

&lt;p&gt;After Project A, our diagnosis was: "The model isn't smart enough. Qwen3 at Q4 quantization can't handle multi-file JWT authentication."&lt;/p&gt;

&lt;p&gt;That diagnosis was wrong — or at least, premature.&lt;/p&gt;

&lt;p&gt;The model appeared to have more usable capability than our system was exposing. In this run, the difference between 0% and 67% looked less like intelligence and more like &lt;strong&gt;context delivery.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This completely changed how we thought about local model limitations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Intelligence Gap&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Information Gap&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Symptom&lt;/td&gt;
&lt;td&gt;Model generates plausible but wrong code&lt;/td&gt;
&lt;td&gt;Model generates structurally incompatible code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagnosis&lt;/td&gt;
&lt;td&gt;"Model too small / too quantized"&lt;/td&gt;
&lt;td&gt;"Prompt missing critical context"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix&lt;/td&gt;
&lt;td&gt;Upgrade model (expensive, diminishing returns)&lt;/td&gt;
&lt;td&gt;Improve information design (free, compounding)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testable?&lt;/td&gt;
&lt;td&gt;Hard — model capability is a black box&lt;/td&gt;
&lt;td&gt;Easy — dump the prompt, count what's missing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Information Design Gap is testable. Dump the prompt. Read it as if you're a developer seeing this project for the first time. If &lt;em&gt;you&lt;/em&gt; couldn't write the code from that prompt alone, the model can't either.&lt;/p&gt;




&lt;h2&gt;
  
  
  Similar Patterns in Recent Research
&lt;/h2&gt;

&lt;p&gt;While writing this post, we surveyed recent research on TDD-based code generation and found similar patterns appearing independently. These don't prove our framework, but the convergence seemed worth noting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alonso et al. (2026)&lt;/strong&gt; tested TDD prompting on SWE-bench Verified with a 30B local model. Adding procedural TDD instructions ("write tests first, then implement") increased regressions. Adding a graph-derived test map ("here are the specific tests at risk") reduced them significantly. Their conclusion: agents don't need to be told &lt;em&gt;how&lt;/em&gt; to do TDD — they need to be told &lt;em&gt;which tests to check.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We saw the same mechanism: telling the model &lt;em&gt;what process to follow&lt;/em&gt; consumed context tokens that could carry &lt;em&gt;actual project information&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Midolo et al. (2026)&lt;/strong&gt; surveyed 50 developers about what makes code generation prompts succeed. Their top factors: algorithmic details (57%) and I/O format specification (44%). When asked what &lt;em&gt;else&lt;/em&gt; was missing, 14% independently reported "contextual information about other components in the system" — which sounds a lot like the gap our per-task context file list was designed to close.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jalil et al. (2025)&lt;/strong&gt; showed that smaller models with TDD and a code interpreter could surpass larger models without those supports. The pattern held across model families: tests as structured context beat model scale.&lt;/p&gt;

&lt;p&gt;Different benchmarks, different teams, different setups. They all point toward the same practical lesson: &lt;strong&gt;before blaming the model, it might be worth inspecting the information pipeline.&lt;/strong&gt; Our data adds one more point in that direction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implications for DCR
&lt;/h2&gt;

&lt;p&gt;In Part 3, I defined DCR as the ratio of deterministic decisions in an agent loop. A reader asked whether DCR should be tracked like test coverage — not just reviewed once at architecture time.&lt;/p&gt;

&lt;p&gt;Running two projects gave us a partial answer: &lt;strong&gt;DCR alone wasn't enough.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ForgeFlow's DCR didn't change between Project A and Project B. It was 85% both times — same 11 of 13 decisions handled deterministically. Yet performance went from 0% to 67%.&lt;/p&gt;

&lt;p&gt;What changed was the &lt;em&gt;quality of information feeding the non-deterministic decisions&lt;/em&gt;. DCR tells you how narrow the model's role is. It doesn't tell you whether the model is equipped to play that role.&lt;/p&gt;

&lt;p&gt;This is why we're now thinking about DCR in two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static DCR&lt;/strong&gt;: how many decision points are designed to be deterministic. (Architecture metric.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observed DCR&lt;/strong&gt;: how many decisions were actually resolved deterministically during real runs. (Runtime metric.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And alongside both: &lt;strong&gt;Information Delivery Rate&lt;/strong&gt; — how much of the available, relevant context actually reaches the model at inference time. Using task-relevant token delivery as a rough proxy, Project A was around 15%. Project B was much closer to the information a human developer would expect to see.&lt;/p&gt;

&lt;p&gt;We're still working out whether these are the right abstractions — but they've been useful for diagnosing our own failures so far.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We're Building Next
&lt;/h2&gt;

&lt;p&gt;The immediate roadmap based on these findings:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RED phase context delivery.&lt;/strong&gt; The RED phase (test generation) was still sending an empty context when we ran these projects. We've since fixed this in the engine — the model now sees existing fixtures before writing new tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic context inference.&lt;/strong&gt; Right now, context files are manually specified per task in the PRD. The next step is deriving them from the dependency graph: if TASK-007 depends on TASK-005 and TASK-006, automatically include their implementation files as context. We're exploring tree-sitter-based approaches for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural mismatch detection.&lt;/strong&gt; Four of twelve tasks didn't fit the RED-GREEN cycle. We want ForgeFlow to detect these patterns (infrastructure setup, fixture-only, test-only) during PRD validation and handle them with a separate path — not force them through TDD.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Thesis, Updated
&lt;/h2&gt;

&lt;p&gt;Part 3's thesis was about structure:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The bottleneck is not model capability, but verifiability of specifications."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two projects later, I'd extend it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;"In our runs, even after we built verifiability, the bottleneck seemed to shift to information delivery — whether the model receives enough context to use that verifiability."&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;DCR gave us the harness. Information design made that harness useful. Both seem to be required. Neither alone was sufficient in our experience.&lt;/p&gt;

&lt;p&gt;Same model. Same engine. Zero code changes. &lt;strong&gt;0% → 67%.&lt;/strong&gt; In our case, the difference was information.&lt;/p&gt;

&lt;p&gt;Several recent studies point in a similar direction, though from different setups. The practical suggestion I'd offer: &lt;strong&gt;if your AI coding agent is underperforming, it might be worth checking what it's receiving before swapping the model.&lt;/strong&gt; That's what worked for us.&lt;/p&gt;




&lt;h2&gt;
  
  
  About
&lt;/h2&gt;

&lt;p&gt;I'm Joseph YEO, a solo builder from Seoul, Korea. ForgeFlow is my experiment in pushing local AI agents toward more reliable autonomous execution — no cloud inference during execution, no hand-holding mid-cycle.&lt;/p&gt;

&lt;p&gt;This post covers what happened when we actually ran the system from Part 3 against real projects and discovered the gap between having a verification harness and feeding the generator enough context. I'm sharing this because I wish someone had told me to dump the raw prompt before I spent weeks blaming the model.&lt;/p&gt;

&lt;p&gt;If you've run into similar issues — or found different solutions — I'd love to hear about it in the comments.&lt;/p&gt;

&lt;p&gt;Follow along:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;𝕏: &lt;a href="https://x.com/josephyeo_dev" rel="noopener noreferrer"&gt;@josephyeo_dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/joseph-yeo" rel="noopener noreferrer"&gt;joseph-yeo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://projectjoseph.dev/" rel="noopener noreferrer"&gt;projectjoseph.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built over ~33 sessions, May 2026. All models run locally via Ollama 0.23.0 on Apple Silicon. No cloud APIs were used during autonomous execution.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was drafted with Claude and edited by me.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
