<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ruthwik Arepelly]]></title><description><![CDATA[Ruthwik Arepelly]]></description><link>https://ruthwikarepelly.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Ruthwik Arepelly</title><link>https://ruthwikarepelly.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 19 Sep 2026 18:13:47 GMT</lastBuildDate><atom:link href="https://ruthwikarepelly.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A Frozen Judge, a Holdout Split, and a Hash Check: What It Actually Takes to Trust an Eval Loop]]></title><description><![CDATA[If you're building any kind of optimization loop over LLM-generated artifacts (prompt search, RAG context tuning, agent config search, an auto-eval pipeline that mutates and re-scores), the part of th]]></description><link>https://ruthwikarepelly.hashnode.dev/a-frozen-judge-a-holdout-split-and-a-hash-check-what-it-actually-takes-to-trust-an-eval-loop</link><guid isPermaLink="true">https://ruthwikarepelly.hashnode.dev/a-frozen-judge-a-holdout-split-and-a-hash-check-what-it-actually-takes-to-trust-an-eval-loop</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[ruthwik arepelly]]></dc:creator><pubDate>Thu, 17 Sep 2026 05:34:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aaaae0fa859e5b0e09ccaf3/2f7be17f-6d1b-4649-9d2d-02b31202d71d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're building any kind of optimization loop over LLM-generated artifacts (prompt search, RAG context tuning, agent config search, an auto-eval pipeline that mutates and re-scores), the part of the system that decides whether it's worth trusting isn't the mutation strategy or the search algorithm. It's the judge. A cheap judge that measures keyword or structure presence instead of correctness will get gamed by any search that's good at optimizing, and it will look like progress while it happens. This is a walkthrough of that failure mode showing up five separate times in one evening, and the architecture that closes it on the sixth attempt.</p>
<h2>Same loop shape, five different artifacts</h2>
<p>The starting hypothesis: a single loop shape, <code>mutate → judge → keep if improved, reject if not → repeat</code>, should generalize across different kinds of agent artifacts. Rather than argue it, five small repos tested it directly, built back to back in about seven minutes of commits (four of the five with exactly one commit each). All six repos in this writeup, the five prototypes and the sixth build below, were built with Claude Code; every commit is paired with a session per the commit trailers, which is the accurate way to describe how this code got written: directed and reviewed, not typed line by line.</p>
<ul>
<li><code>self-improving-prompt-agent</code>: mutates <code>prompt.txt</code> from a fixed candidate list, scored by a heuristic judge in 0.0–1.0.</li>
<li><code>context-improving-agent</code>: mutates <code>context.txt</code>, scored by <code>0.8 * fact_coverage + 0.2 * (1 - bloat_penalty)</code>.</li>
<li><code>graph-improving-agent</code>: mutates a workflow graph (<code>{nodes, edges}</code> in <code>graph.json</code>), inserting one role node per round, scored on role-keyword presence.</li>
<li><code>harness-improving-agent</code>: mutates an instruction block, adding one guardrail from a fixed list of ten, scored on keyword coverage across those ten.</li>
<li><code>agent-improving-agent</code>: a level up from the other four. Instead of mutating an artifact, it mutates the <em>priority order</em> <code>harness-improving-agent</code> tries its ten guardrail candidates in, scored on weighted coverage inside a fixed 5-of-10 round budget. Its README states the real constraint directly: eval budgets are scarce, and if you only get five shots, which five guardrails should go first is a legitimate product question, not just a toy one.</li>
</ul>
<p>Structurally, the hypothesis held: four of the five READMEs restate a near-identical <code>edit → evaluate → keep/reject → repeat</code> sentence, applied to a prompt, a context block, a graph, a harness, and a meta-level ordering problem over another loop's candidates.</p>
<h2>The numbers, and why the climb doesn't mean what it looks like</h2>
<p>Ten rounds each:</p>
<table>
<thead>
<tr>
<th>Repo</th>
<th>Result</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td><code>self-improving-prompt-agent</code></td>
<td>0.10 → 0.80</td>
<td>3/10 rounds duplicate mutations, correctly rejected</td>
</tr>
<tr>
<td><code>context-improving-agent</code></td>
<td>0.20 → 0.28</td>
<td>1/10 kept (a pricing-info addition), 9 rejected</td>
</tr>
<tr>
<td><code>graph-improving-agent</code></td>
<td>0.10 → 0.70</td>
<td>7 kept</td>
</tr>
<tr>
<td><code>harness-improving-agent</code></td>
<td>0.00 → 0.90</td>
<td>9 kept, 1 duplicate rejected. An early version of the judge used loose keyword matching and stalled at 0.20; the judge's own wording was rewritten mid-run to match the mutation wording it was supposed to score. Part of the 0.90 is that fix, not search progress. The README says this directly.</td>
</tr>
<tr>
<td><code>agent-improving-agent</code></td>
<td>0.45 → 0.61 (vs. a stated 0.80 max)</td>
<td>2/10 swaps kept, 7 rejected, 1 regressed to 0.42 and was correctly rejected</td>
</tr>
</tbody></table>
<p>A rising score curve here is not evidence the underlying prompt, context, graph, or harness got better in any real sense. It's evidence the artifact got better at satisfying a judge that measures the presence of the right words or shapes, not correctness or usefulness. <code>graph-improving-agent</code>'s judge scores whether a role keyword like "verifier" is attached to a node, with no check that the resulting graph is executable or that the node receives the right inputs. A syntactically valid but functionally inert verifier node scores identically to a useful one, because the judge never looks past the label. <code>harness-improving-agent</code>'s guardrail-keyword coverage has the same shape: a harness that contains the word "rollback" scores the same whether or not any rollback mechanism exists.</p>
<p>Every one of the five repos discloses this in its own README (<code>graph-improving-agent</code>: "no check that the wiring is actually runnable/useful"; <code>harness-improving-agent</code>: "not a real check that the agent obeys the stated guardrails at runtime"). Read individually, each disclosure looks like a minor, self-aware caveat. Read together, it's the same design flaw appearing five times under five different names. That's the thing worth building a response to.</p>
<h2>What changes in the sixth build</h2>
<p>About two and a half hours after the fifth prototype's commit, a sixth repo (<code>self-improving-agent</code>, internally "sia") was built that explicitly treats the first five as prior art it's correcting. Its README states the reasoning for not reusing their judges directly: doing so "would have meant the keep/rollback gate approved patches for saying the right words rather than answering questions correctly."</p>
<p>Four architectural changes, each closing a specific failure mode from the first five:</p>
<p><strong>A frozen, deterministic judge, scored against held-out data.</strong> <code>sia</code> optimizes a <code>genome</code>, a versioned JSON config with <code>prompt</code> / <code>context</code> / <code>workflow</code> / <code>harness</code> layers, against a fixed 8-task eval set split 5 train / 3 holdout. The judge doesn't get rewritten mid-run, unlike <code>harness-improving-agent</code>'s.</p>
<p><strong>A real gate condition instead of a threshold on one number:</strong></p>
<pre><code>keep iff train_child &gt;= train_parent + 1.0
         AND holdout_child &gt;= holdout_parent
</code></pre>
<p>The <code>+1.0</code> minimum lift on train, combined with a non-regression requirement on holdout, exists specifically to catch train-only overfitting: a patch that improves the five training tasks by exploiting some quirk of those exact examples, without generalizing, gets caught by the holdout half of the condition. None of the first five repos had any holdout set at all; they scored and kept against the exact signal they optimized against, every round.</p>
<p><strong>Anti-cheating integrity checks.</strong> <code>sia/loop.py</code> hashes the eval set and the judge file at the start of a run and re-checks the hash after every iteration, aborting with a logged <code>ABORT</code> row if either changed. This is a direct response to having just watched five judges get gamed by the cheapest possible strategy: an optimizer that's allowed to edit what it's graded against will eventually find that changing the test is cheaper than improving the artifact.</p>
<p><strong>A control test, not just a demo run.</strong> Rather than trust a printed summary, the results were checked directly against <code>sia</code>'s own ledger (<code>runs/ledger.jsonl</code>):</p>
<ul>
<li>Context patch: train 0.0 → 50.0, holdout 0.0 → 33.33: <strong>KEPT</strong></li>
<li>Harness patch: train 50 → 70, holdout 33.33 → 66.67: <strong>KEPT</strong></li>
<li>Workflow patch: train 70 → 90, holdout 66.67 → 100: <strong>KEPT</strong></li>
<li>A deliberately-constructed negative-control patch with zero train lift: <strong>ROLLBACK</strong></li>
</ul>
<p>Three KEEPs and one ROLLBACK means both a positive and a negative control passed, which is a stronger claim than "the score went up": it's evidence the gate distinguishes real lift from no lift, not just that it produces an increasing number. Alongside that, all 21 of the project's unit tests pass (<code>python -m pytest tests/ -v</code>), re-run live rather than taken from a log.</p>
<p>One scope decision is worth calling out on its own: the README states plainly that the model is "not allowed to call itself better." The improver component can write a hypothesis or diagnosis of why a patch might help, but that text stays documentation only; it never becomes a scoring input. That closes off a failure mode where a model's own confident narration substitutes for a real measurement, which is structurally close to the same failure a keyword-presence judge exhibits: rewarding the appearance of a good answer instead of verifying it.</p>
<h2>What this doesn't prove</h2>
<p>Everything above happened in mock mode. <code>sia</code>'s worker is a simulated stand-in, not a live Claude API call. A <code>.env</code> file has an API key present, but nothing in the repo, no log, no genome, no ledger row, is attributable to a real run. <code>sia/claude.py</code>'s <code>ClaudeClient</code> hard-raises if it's ever invoked while <code>mock=True</code>, so there's no ambiguity about which mode produced the verified result above. Whether the gate condition, the holdout split, or the hash-check system behave the same way under real, noisy, non-deterministic model output is genuinely open. It's untested.</p>
<p>There's also an unfinished piece: <code>agent-improving-agent</code>'s meta-loop, the one optimizing another loop's candidate priority order, was designed to plug into exactly this kind of frozen judge and never got wired in. The README says as much directly: it's future work. The arc stops mid-thought, not at a closed loop.</p>
<p>Neither of these caveats is a hedge added after the fact. Both are stated in the source repos themselves, and both matter more than the headline result if you're deciding whether any of this architecture is something to borrow.</p>
<h2>The part that generalizes</h2>
<p>If you're building an optimization loop over LLM output of any kind, the order of operations here is the transferable piece, not the specific repo layout. Before investing in a better mutation strategy, a smarter search, or more rounds, check what your judge is actually measuring and how cheap it is to satisfy without the artifact actually being good. A keyword-presence judge gets gamed eventually, by an evolutionary search, a gradient step, or an LLM proposing patches, not because the search is adversarial, but because "score well against this specific signal" is a much easier target than "be correct," and any capable optimizer finds the easier target first. A frozen judge, a real holdout split, and an integrity check on the eval set itself aren't a smarter loop. They're what it takes to make the loop's output mean something at all, and even then, only under the conditions actually tested here: mock mode, one evening, eight tasks.</p>
]]></content:encoded></item></channel></rss>