<?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: the kilted dev</title>
    <description>The latest articles on DEV Community by the kilted dev (@thekilteddev).</description>
    <link>https://dev.to/thekilteddev</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%2F4030958%2Fc40140e1-f5ac-4668-9b6b-166ea0ddc762.png</url>
      <title>DEV Community: the kilted dev</title>
      <link>https://dev.to/thekilteddev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thekilteddev"/>
    <language>en</language>
    <item>
      <title>The file nobody reread</title>
      <dc:creator>the kilted dev</dc:creator>
      <pubDate>Sat, 05 Sep 2026 18:00:47 +0000</pubDate>
      <link>https://dev.to/thekilteddev/the-file-nobody-reread-5d5</link>
      <guid>https://dev.to/thekilteddev/the-file-nobody-reread-5d5</guid>
      <description>&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/L_jSpfwmQ7s" width="315" height="560"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;Field notes from a hosting migration for an NHS food-diary proof of concept. A certificate checker&lt;br&gt;
was quietly lying about whether a fix had worked, and two sessions rejected the same hosting idea&lt;br&gt;
twice, four days apart, without either reading the other's reasoning. The move itself was still on&lt;br&gt;
hold when this was written.&lt;/p&gt;

&lt;p&gt;This is a proof-of-concept, not an NHS product, built by one technical person working alongside an&lt;br&gt;
NHS dietetic team. No patient data exists anywhere in it. The app stores everything on the&lt;br&gt;
client device and nothing else. The trigger for all of this was mundane: Netlify's free tier caps&lt;br&gt;
a project at 15 credits per production deploy, 300 a month, which works out to roughly twenty&lt;br&gt;
deploys before the meter runs dry. The project was already rationing pushes against that ceiling,&lt;br&gt;
and shared hosting on Namecheap, already being set up for another project the same person runs,&lt;br&gt;
looked like a way out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two rejections, four days apart
&lt;/h2&gt;

&lt;p&gt;GitHub Pages came up early, and the project turned it down fast. On the free plan it only serves&lt;br&gt;
public repositories. A private repo needs a paid GitHub plan, and even then the site still&lt;br&gt;
publishes to a public URL with no login wall short of Enterprise. Netlify's free tier deploys from&lt;br&gt;
a private repo to an unlisted one instead, which fit the stated preference to keep the code itself&lt;br&gt;
private. No patient data was ever going to sit in that repo either way. The preference was about&lt;br&gt;
the source, not about anything a patient had typed.&lt;/p&gt;

&lt;p&gt;Four days later, GitHub Pages came up again. Not from a different person with a different view. A&lt;br&gt;
coding session working on the project proposed the exact idea an earlier session had already&lt;br&gt;
rejected, because nobody had reread the decisions file before speaking. The occasion was almost&lt;br&gt;
incidental: a different, newer domain had surfaced, already live on GitHub Pages with a working&lt;br&gt;
certificate, and for a moment it looked like an obvious destination. That session looked into it,&lt;br&gt;
then declined it again, for the same reason as the first time. That reason had not moved. It had&lt;br&gt;
simply never been reread. This round did turn up one genuinely new blocker: the food-diary repo&lt;br&gt;
and the newer domain lived under two different GitHub accounts, and GitHub blocks a custom domain&lt;br&gt;
from being claimed by a second account once it's verified on the first, specifically to stop that&lt;br&gt;
kind of takeover. A real finding, arrived at by accident, in the course of re-litigating a question&lt;br&gt;
that had already been settled.&lt;/p&gt;

&lt;p&gt;A decisions log nobody rereads is decoration. It was there, it had the right answer sitting in it,&lt;br&gt;
and a session still asked the question again anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  A certificate checker that had been lying
&lt;/h2&gt;

&lt;p&gt;The SSL side had its own real bug and its own fake one. The real bug: the domain's nameservers&lt;br&gt;
were pointed at Namecheap's basic DNS, which only handles URL forwarding, not actual hosting.&lt;br&gt;
Switched to the proper hosting DNS and that half resolved. An earlier assumption, picked up from a&lt;br&gt;
web search during planning, claimed the hosting tier included self-service AutoSSL. It didn't.&lt;br&gt;
Triggering AutoSSL turned out to require WHM-level (the hosting provider's own backend) server-administrator access, which an ordinary&lt;br&gt;
shared-hosting account never gets. That was confirmed against Namecheap's own documentation once&lt;br&gt;
the free "Run AutoSSL" button turned out not to exist anywhere in the dashboard. The free certificate that does ship&lt;br&gt;
with the hosting plan has to be issued from Namecheap's own side, and nothing on the customer's end&lt;br&gt;
can force it.&lt;/p&gt;

&lt;p&gt;The fake bug was worse, because it looked like a real one for a while. This laptop's own antivirus&lt;br&gt;
intercepts TLS connections and reissues every certificate itself, so anything running on the&lt;br&gt;
machine, &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;openssl&lt;/code&gt; included, was reporting the antivirus's own certificate instead&lt;br&gt;
of the real one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The tool being used to answer "has the SSL fix landed yet" could not tell the truth about it even&lt;br&gt;
in principle.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Checking from a phone worked around it, and later a tool that runs the request server-side,&lt;br&gt;
outside the interception entirely, confirmed it for good. Two support agents tried&lt;br&gt;
force-validating the certificate from Namecheap's own side with no result, and a follow-up ticket&lt;br&gt;
went in to track it as a pure backend wait. Nothing on the customer side could shortcut that part.&lt;br&gt;
The certificate issued not long after, and a server-side check confirmed clean HTTPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things the plan hadn't accounted for
&lt;/h2&gt;

&lt;p&gt;First: SSH turned out to be off by default on shared hosting, though the migration plan had been&lt;br&gt;
written without confirming that. The fix was self-service, a control panel option called "Manage&lt;br&gt;
Shell," which gives no hint it's SSH, listening on a non-standard port rather than the usual one. Second:&lt;br&gt;
pushing to the new remote from an automated shell hung indefinitely with no error, because a global&lt;br&gt;
credential setting that keeps two separate GitHub accounts from colliding also blocks the plain&lt;br&gt;
per-repo push path unless a repo-level override is set. That global setting has to stay exactly as&lt;br&gt;
it is. The only new part is the missing per-repo override.&lt;/p&gt;

&lt;p&gt;A smaller trap surfaced once both hosts were finally level with GitHub again: the control panel's&lt;br&gt;
Git tool has an "Update" button that only saves configuration. It doesn't fetch, pull, or deploy&lt;br&gt;
anything, and those actions live on a separate tab entirely. For a repository that was deliberately&lt;br&gt;
cloned through the control panel's own "Clone a Repository" tool, with a specific option left unchecked to&lt;br&gt;
preserve its automatic deploy hook, even that tab's own "Update from Remote"&lt;br&gt;
option turns out to be a no-op. It's a push target, not a pull source. Reading the UI correctly&lt;br&gt;
took longer than reading the DNS panel had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting a task is not a local edit
&lt;/h2&gt;

&lt;p&gt;One step in the migration plan retired Netlify outright once the new host was live. Once Namecheap&lt;br&gt;
turned out to be an interim stop rather than the final destination, the task was dropped, which&lt;br&gt;
seemed like a small, contained change. It wasn't. That same task had been referenced as a&lt;br&gt;
precondition in five other places across the plan, and one of those was dangerous on its own: a&lt;br&gt;
later step told a future session to delete the Netlify credit-cap constraint from the project's own&lt;br&gt;
guide file, once hosting had moved. Hosting hadn't moved. A session following that instruction&lt;br&gt;
literally would have silently removed a still-live limit from the record that exists to track it.&lt;br&gt;
All five references were found and corrected before that could happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed the private-repo rule
&lt;/h2&gt;

&lt;p&gt;The preference that had driven both GitHub Pages rejections wasn't overturned by a technical&lt;br&gt;
argument. It was relaxed on purpose, once the user actually named a real gap instead of waving it&lt;br&gt;
off: the diary's free-text fields, a description typed next to a photo, have no structural way to&lt;br&gt;
stop someone entering something identifying. Going public meant accepting that gap rather than&lt;br&gt;
pretending it away. In the user's own words, it was "hard to guard... loose enough to say go&lt;br&gt;
public." The account-split blocker from the second rejection turned out not to matter here either,&lt;br&gt;
since the plan kept the repo under its existing account rather than moving it to the newer brand's.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bigger swap, investigated instead of just refused
&lt;/h2&gt;

&lt;p&gt;A smaller question, whether a branded subdomain could dodge a shared hosting username, led&lt;br&gt;
somewhere larger: making the newer, higher-traffic domain the account's designated "Main Domain,"&lt;br&gt;
on the assumption that the label itself would grant it more subdomain headroom. That assumption&lt;br&gt;
didn't survive a check. The account's subdomain quota is shared regardless of which domain holds&lt;br&gt;
that label, so an ordinary addon domain gets exactly the same headroom. What an&lt;br&gt;
actual swap would require turned out to be much bigger than the label suggested: a different&lt;br&gt;
project's own blog would have to leave its free, automatic hosting for manual shared-hosting&lt;br&gt;
deploys with none of that automation and the same multi-day certificate process already documented&lt;br&gt;
here. That's not a call this project gets to make about another one. It was declined without&lt;br&gt;
touching anything, and the idea went out as two information-only notes to the two other projects'&lt;br&gt;
own owners, for them to decide on their own terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually landed
&lt;/h2&gt;

&lt;p&gt;The reversal, when it came, was decided outside any of these sessions, by the one person who owns&lt;br&gt;
all three projects at once: Netlify was retired entirely, and the food diary moved to GitHub Pages&lt;br&gt;
after all, kept under its existing account rather than a newer pseudonymous one. That keeps a tool&lt;br&gt;
built under a real identity separate from a brand built under a pseudonym, and it sidesteps the&lt;br&gt;
cross-account domain restriction outright rather than working around it.&lt;/p&gt;

&lt;p&gt;The account question that had paused the move turned out simpler than the plan in flight at the&lt;br&gt;
time suggested. Rather than standing up a new account, the existing one kept its history and its&lt;br&gt;
repos exactly where they were and simply changed its own display name. Same account throughout,&lt;br&gt;
just renamed partway through. The migration is complete now. Both old hosts are fully retired, and&lt;br&gt;
nothing is left dual-hosted.&lt;/p&gt;

&lt;p&gt;It ended without a clever fix. Two things went wrong here that weren't code. Both were caught the&lt;br&gt;
same way, by someone who actually checked instead of assuming. A certificate check that had every&lt;br&gt;
reason to look trustworthy was quietly answering a different question than the one being asked. And&lt;br&gt;
a written decision only held once someone actually reopened the file before speaking a second time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://thekilted.dev/nobody-reread/" rel="noopener noreferrer"&gt;thekilted.dev/nobody-reread&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hosting</category>
      <category>devops</category>
      <category>buildinpublic</category>
      <category>debugging</category>
    </item>
    <item>
      <title>The verdict that survived its own correction</title>
      <dc:creator>the kilted dev</dc:creator>
      <pubDate>Sun, 30 Aug 2026 06:15:27 +0000</pubDate>
      <link>https://dev.to/thekilteddev/the-verdict-that-survived-its-own-correction-50gd</link>
      <guid>https://dev.to/thekilteddev/the-verdict-that-survived-its-own-correction-50gd</guid>
      <description>&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/A2RhXAwQORo" width="315" height="560"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;This project started with a conclusion already sitting in its lap. In June, an earlier stretch of&lt;br&gt;
work on the same machine had tested whether a local model could handle iterative coding: writing&lt;br&gt;
files, running tests, fixing what broke, without a human shuttling text between a chat window and&lt;br&gt;
an editor. The verdict was no. Not viable. That was the finding the whole project was built on.&lt;/p&gt;

&lt;p&gt;On 4 July, someone re-tested it. It fell over in about two minutes.&lt;/p&gt;

&lt;p&gt;The interesting part isn't that the verdict was wrong. It's that the verdict was right. It had&lt;br&gt;
been stated precisely, with its own explanation attached, and none of that survived the trip into&lt;br&gt;
the new project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The re-test
&lt;/h2&gt;

&lt;p&gt;The task was a word search game: three files, a 10x10 grid, eight words hidden horizontally,&lt;br&gt;
vertically or diagonally, click-and-drag to select, correct words struck through in the list, a&lt;br&gt;
New Game button. Eight scored criteria. The eighth is the one that matters: ask for one follow-up&lt;br&gt;
fix (make found words green) and check it lands without breaking the drag highlighting that&lt;br&gt;
already worked. That regression check is what had killed every previous run.&lt;/p&gt;

&lt;p&gt;Run through &lt;code&gt;little-coder&lt;/code&gt;, a small harness that gives the model real write and read tools instead&lt;br&gt;
of asking it to print code into a chat window, against &lt;code&gt;qwen3-coder-30b&lt;/code&gt; served by Ollama, the&lt;br&gt;
result was 8 out of 8 in 2.1 minutes. The follow-up fix took another 30 seconds. All of it verified&lt;br&gt;
in a real browser with simulated drag events, not taken from the model's own account of what it had&lt;br&gt;
done.&lt;/p&gt;

&lt;p&gt;The same benchmark against the smaller &lt;code&gt;qwen3.5-9b&lt;/code&gt; scored 7.5 out of 8 in 44.1 minutes. It lost&lt;br&gt;
half a point on reverse-placed and diagonal words: no reversed-string check, an off-by-one in the&lt;br&gt;
highlight loop. It still passed the regression test, the first local setup here to manage that.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;qwen3-coder-30b&lt;/td&gt;
&lt;td&gt;8/8&lt;/td&gt;
&lt;td&gt;2.1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qwen3.5-9b&lt;/td&gt;
&lt;td&gt;7.5/8&lt;/td&gt;
&lt;td&gt;44.1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The quality gap was only half a point; the time gap was a full 42 minutes.&lt;/p&gt;

&lt;p&gt;Two honest marks sit against the 8 out of 8. One transient failure died with "Stream ended without&lt;br&gt;
finish_reason" and an identical retry worked. On criterion 8, one green pixel was never confirmed,&lt;br&gt;
because the preview tab had collapsed to a zero-width viewport and inline styles had stopped&lt;br&gt;
applying: impossible in a real browser, so it was scored as a harness fault rather than a code&lt;br&gt;
fault. That was a judgment call, made and written down as one at the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this project wrote down that evening
&lt;/h2&gt;

&lt;p&gt;The June verdict was a harness problem, not a model problem.&lt;/p&gt;

&lt;p&gt;It's a clean line. It reads as an insight. It went into the project's working memory as a headline&lt;br&gt;
finding, was quoted straight into the active constraints file, and shaped everything scoped&lt;br&gt;
afterward: if the harness is the variable that matters, you invest in harnesses and stop shopping&lt;br&gt;
for models.&lt;/p&gt;

&lt;p&gt;The investment was right. The claim wasn't, and nobody checked it for two weeks, because a&lt;br&gt;
conclusion that's useful and turning out well doesn't feel like it needs auditing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the original verdict actually said
&lt;/h2&gt;

&lt;p&gt;The June work had been handed over as a document. Reading it properly, two weeks late, the verdict&lt;br&gt;
is one row in a table of six use cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Locally viable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code CLI backend&lt;/td&gt;
&lt;td&gt;✕ broke — models misinterpret Claude Code's own system prompt format, not a config problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-file document extraction (LM Studio + Gemma 4 E4B, 8192 ctx)&lt;/td&gt;
&lt;td&gt;✓ met — works well, the best multi-file result of anything tried&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-file extraction, agent + filesystem (Goose + Ollama, qwen3.5-9b)&lt;/td&gt;
&lt;td&gt;✓ met — works, but slow: 62 seconds per file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iterative coding (any local chat UI)&lt;/td&gt;
&lt;td&gt;✕ broke — not viable, and structural: five failure modes, not a fixable bug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single-turn code generation (Goose or LM Studio, max context)&lt;/td&gt;
&lt;td&gt;✓ met — viable as a baseline handoff into a human editor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quota fallback for real sessions (fcc-server)&lt;/td&gt;
&lt;td&gt;✕ broke — routed to Gemini instead, local wasn't trusted as the fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;June's headline verdicts, all six, from the same document that scoped iterative coding to "any&lt;br&gt;
local chat UI." The row that got re-tested is one of six, not the whole page.&lt;/p&gt;

&lt;p&gt;The scope is in the verdict line. Not "local models cannot do iterative coding." Local models&lt;br&gt;
cannot do iterative coding in a chat UI, which is a claim about an interface.&lt;/p&gt;

&lt;p&gt;Below it, five structural failure modes are listed. Stateless regression, where a fix in turn N&lt;br&gt;
gets silently undone in turn N+1: observed, in that same word search task, as a drag highlighter&lt;br&gt;
fixed and then broken. Overconfident reporting. Silent context truncation. Effective context not&lt;br&gt;
matching advertised. No cross-session persistence.&lt;/p&gt;

&lt;p&gt;And then, closing that section:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why Claude Code avoids 1–2: tool-mediated verification per step, persistent conversation&lt;br&gt;
context — architecture, not raw model capability.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;June had already worked out that the constraint was architectural. It's written down, in the&lt;br&gt;
document that was handed over, in bold, five weeks before the July re-test that "discovered" it.&lt;/p&gt;

&lt;p&gt;July didn't overturn June. July supplied the harness June had described, and measured what&lt;br&gt;
happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where June was too optimistic
&lt;/h2&gt;

&lt;p&gt;Of the five failure modes, June expected a harness to handle the first two: stateless regression&lt;br&gt;
and overconfident reporting, both covered by tool-mediated verification and persistent context.&lt;/p&gt;

&lt;p&gt;The first half held. The second didn't.&lt;/p&gt;

&lt;p&gt;Overconfident reporting is a model claiming completion when a feature doesn't actually work, and a&lt;br&gt;
harness only catches that if its tests are adversarial. This project has watched a weak test&lt;br&gt;
launder a false claim into a green, twice. A redaction check passed with&lt;br&gt;
&lt;code&gt;assert "84210.55" not in out&lt;/code&gt; while a partially redacted amount, &lt;code&gt;842[AMOUNT]&lt;/code&gt;, leaked straight&lt;br&gt;
through it. A loader passed nineteen of nineteen with a leaked loop variable, masked by a fixture&lt;br&gt;
that only ever exercised one file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Real file tools, persistent context, tests that ran green: both greens were wrong.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So June wasn't simply more careful than its own summary. On this one point it was optimistic, and&lt;br&gt;
the correction only turned up once this project built the harness June had described and ran it&lt;br&gt;
long enough to get burned.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually went wrong
&lt;/h2&gt;

&lt;p&gt;Four things actually changed between June and July, worth stating because it means the re-test was&lt;br&gt;
never a controlled experiment. The model changed. The harness changed. The runtime flags changed&lt;br&gt;
(July ran with flash attention and a quantised key-value cache, June predated both). And the prompt&lt;br&gt;
changed, because June's prompt text was never preserved and had to be rebuilt from scratch.&lt;/p&gt;

&lt;p&gt;That last one isn't carelessness. The June raw evaluation file survives, and it's 809 bytes: a&lt;br&gt;
findings summary whose entire task description is one line. There was no prompt left to preserve by&lt;br&gt;
the time anyone went looking. Reconstruction was the only option on the table.&lt;/p&gt;

&lt;p&gt;But the four variables are a side issue. The real failure was compression.&lt;/p&gt;

&lt;p&gt;What travelled from June into this project was the phrase "iterative coding not viable." The scope,&lt;br&gt;
any local chat UI, didn't make the trip. Neither did the mechanism: architecture rather than raw&lt;br&gt;
model capability.&lt;/p&gt;

&lt;p&gt;Then someone re-tested the compressed form, found it wanting, and corrected it into a claim broader&lt;br&gt;
and less accurate than the original. A precise, well-reasoned, correctly scoped verdict went in one&lt;br&gt;
end and came out the other as a slogan, and the slogan is what the project ended up arguing with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The verdict, June:&lt;/strong&gt; Iterative coding — not viable (structural, see the five failure modes) — any&lt;br&gt;
local chat UI. Scoped to an interface, with its own mechanism named: architecture, not raw model&lt;br&gt;
capability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What travelled:&lt;/strong&gt; "Iterative coding not viable." The scope and the mechanism both stayed behind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What got re-tested:&lt;/strong&gt; "Harness, not model." Broader and less accurate than the verdict it&lt;br&gt;
replaced, and the claim this project ended up arguing with.&lt;/p&gt;

&lt;p&gt;The handover document's own header says it plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nothing here needs re-deriving — it was all tested hands-on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This project re-derived it anyway, and ended up with a worse version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that only appeared while writing this
&lt;/h2&gt;

&lt;p&gt;Writing this piece meant going back to the June sources, and the first attempt to find them failed.&lt;br&gt;
The active constraints file listed the handover document under a path in the other project, the one&lt;br&gt;
that had written it. It wasn't there. Searching that project turned up nothing.&lt;/p&gt;

&lt;p&gt;The conclusion drawn from that was that both June primaries had been deleted. That conclusion went&lt;br&gt;
into a correction to this project's own files, into an early draft of this piece, and into a&lt;br&gt;
request asking another project to recover them from git history.&lt;/p&gt;

&lt;p&gt;The document was sitting in this project's own repository root. It had been there since the first&lt;br&gt;
commit. It had been written elsewhere and moved across at handover, exactly as its own header says.&lt;br&gt;
The raw evaluation file had moved into a shared knowledge vault during a migration weeks earlier,&lt;br&gt;
and was just as findable. The other project located both files in minutes, and mentioned, almost as&lt;br&gt;
an aside, that it has no git history at all, so the recovery being asked for could never have worked&lt;br&gt;
anyway.&lt;/p&gt;

&lt;p&gt;One failed lookup at one stale path became "gone from disk," and a fair amount was built on top of&lt;br&gt;
that before anyone checked the obvious place.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That's the same mistake this piece is about, made while writing about it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An absence, asserted from a single negative result. A conclusion, drawn from a summary (the path in&lt;br&gt;
the constraints file) rather than from the thing itself.&lt;/p&gt;

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

&lt;p&gt;The finding was rewritten to what the evidence actually carries. The harness thesis is June's, not&lt;br&gt;
this project's, and it was right. What this project added is confirmation and a measurement. The&lt;br&gt;
broad version, "harness, not model," is now marked as over-stated, because it took this project's&lt;br&gt;
own later evidence, not June's, to find the one place a harness alone wasn't enough.&lt;/p&gt;

&lt;p&gt;The practical guidance didn't change at all, which is the uncomfortable part. Invest in the&lt;br&gt;
harness. Verify with real tool output. Keep the local tier for bounded work. All still correct, all&lt;br&gt;
still what this project does. A claim can be over-stated and still point the right way, and that&lt;br&gt;
combination is exactly what stops anyone from checking it again.&lt;/p&gt;

&lt;p&gt;Two rules came out of this, both narrow and mechanical. Before re-testing an inherited verdict,&lt;br&gt;
read the primary document, not your own summary of it: an active-constraints file strips scope and&lt;br&gt;
exceptions by design, because stripping detail is what it's for. And an absence needs more than one&lt;br&gt;
failed lookup, especially when the path you searched came from the same compressed source you're&lt;br&gt;
already re-deriving from.&lt;/p&gt;

&lt;p&gt;What travels between sessions, projects and people is the conclusion. The conditions that produced&lt;br&gt;
it stay behind. A conclusion without its conditions is a rumour with a number attached.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://thekilted.dev/the-verdict-that-survived-its-own-correction/" rel="noopener noreferrer"&gt;thekilted.dev/the-verdict-that-survived-its-own-correction&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>localmodels</category>
      <category>llm</category>
      <category>softwareengineering</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Protected nothing</title>
      <dc:creator>the kilted dev</dc:creator>
      <pubDate>Sun, 23 Aug 2026 13:52:56 +0000</pubDate>
      <link>https://dev.to/thekilteddev/protected-nothing-2fgc</link>
      <guid>https://dev.to/thekilteddev/protected-nothing-2fgc</guid>
      <description>&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/r5SC-Dyw2o4"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;Field notes from a proof-of-concept food-diary tool built alongside one NHS dietetic team,&lt;br&gt;
not an official NHS product, no patient data anywhere in it. A privacy guardrail blocked its&lt;br&gt;
repository from going public. Traced through the code, it protected nothing that going public would&lt;br&gt;
have exposed.&lt;/p&gt;

&lt;p&gt;A rule sounded exactly right, protected exactly nothing, and stood as the one thing blocking real&lt;br&gt;
work, until someone traced it back to what the code actually does.&lt;/p&gt;

&lt;p&gt;The rule was simple. Before the food diary's repository could go public, it needed a guardrail&lt;br&gt;
against identifiable data leaking through the diary's own free-text fields, chiefly a description a&lt;br&gt;
client might type next to a reference photo. Someone could type their own name into it without&lt;br&gt;
thinking, to help remember whose entry it was. That risk was real, agreed on at the time in the&lt;br&gt;
project's own words as "hard to guard... loose enough to say go public," and it sat there as the one&lt;br&gt;
condition standing between the repository and a public GitHub Pages migration everything else was&lt;br&gt;
ready for.&lt;/p&gt;

&lt;p&gt;The condition held for a day. Then, traced through the actual code rather than argued about again,&lt;br&gt;
it didn't survive the first look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the description actually goes
&lt;/h2&gt;

&lt;p&gt;The description a client types is captured in the app, stored as a note on a log entry in IndexedDB&lt;br&gt;
on the device, and exported into a spreadsheet the dietitian handles. It is never committed, never&lt;br&gt;
pushed, never served. The repository's visibility has no effect on it whatsoever, because it was&lt;br&gt;
never in the repository to begin with. A guardrail on that field would have protected data that&lt;br&gt;
going public put at no risk at all.&lt;/p&gt;

&lt;p&gt;A second, independent reason cut just as deep. The app ships with no build step at all, just&lt;br&gt;
straight unminified JavaScript, so its entire source has been downloaded verbatim by every single&lt;br&gt;
visitor to the live site since the day it launched. Repo visibility was never protecting the&lt;br&gt;
application code either, because the browser was already handing that code to anyone who asked. The&lt;br&gt;
"code stays private" preference behind two earlier hosting rejections had, for the part of the&lt;br&gt;
project a visitor could actually reach, never once been true.&lt;/p&gt;

&lt;h2&gt;
  
  
  The place nobody checked
&lt;/h2&gt;

&lt;p&gt;If the free-text field was never the risk, going public should have been safe from day one. It&lt;br&gt;
wasn't, quite, and the reason was somewhere else entirely: roughly forty tracked files sitting&lt;br&gt;
outside the application folder, never served to a visitor and shielded from view by nothing but the&lt;br&gt;
repository itself being private, among them migration notes, deploy docs, session logs, and a stray&lt;br&gt;
config file. Grepping through all of it for anything infrastructure-shaped turned up exactly one&lt;br&gt;
genuinely sensitive string: a hosting-account username, and not even this project's own account. It&lt;br&gt;
belonged to someone else's.&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;In the repo&lt;/th&gt;
&lt;th&gt;Repo privacy hides it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free-text descriptions&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application source&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~40 files outside the app folder&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things the repository question could have been about. Only the last is both tracked in the&lt;br&gt;
repository and hidden by nothing else, so it is the only one that going public actually exposes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The thing everyone had been scared of couldn't actually happen. The thing worth being scared of&lt;br&gt;
had been sitting in a folder nobody had opened, the whole time the guardrail argument was running.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A fix that doesn't depend on remembering
&lt;/h2&gt;

&lt;p&gt;The fix that shipped doesn't ask anyone to remember anything. Rather than scrub the sensitive files&lt;br&gt;
out of a single shared repository and trust every future session to keep scrubbing, the project&lt;br&gt;
split in two: one private repository holding everything, full history intact, and one public&lt;br&gt;
repository synced from it that holds only the application directory's own contents. Nothing outside&lt;br&gt;
&lt;code&gt;app/&lt;/code&gt; can leak, because nothing outside &lt;code&gt;app/&lt;/code&gt; is ever in the public repo to begin with.&lt;/p&gt;

&lt;p&gt;Purging the sensitive files from one shared repository's history was considered and turned down on&lt;br&gt;
purpose. &lt;code&gt;NOTES.md&lt;/code&gt; gets written every session, which means every future session is a fresh chance&lt;br&gt;
to type an identifier back into a file that's now public, and a miss like that is silent and&lt;br&gt;
unrecoverable the moment the repository is cloned. The split doesn't have that failure mode. There's&lt;br&gt;
nothing to miss, because the sensitive files are never in the repo that goes public in the first&lt;br&gt;
place.&lt;/p&gt;

&lt;p&gt;One faster fix was on the table too, and turned down for a completely different kind of reason.&lt;br&gt;
GitHub offers private-repo Pages hosting on its paid tier, for around four dollars a month, which&lt;br&gt;
would have made the whole question go away without touching a single file. Food diary's own author&lt;br&gt;
turned it down outright, on the spot, with two words: "not pro." No technical argument followed it,&lt;br&gt;
because none was needed. It was a spending decision, not an engineering one, and the project didn't&lt;br&gt;
pretend otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the guardrail actually protects now
&lt;/h2&gt;

&lt;p&gt;The free-text field is still a real concern. It existed before this repository was ever considered&lt;br&gt;
for public release, and it will keep existing after, because it's a property of what the app lets&lt;br&gt;
someone type, not of who can see the code. It just isn't a hosting decision, and treating it as one&lt;br&gt;
is the mistake this piece is actually about. It has its own design question to answer, on its own&lt;br&gt;
schedule, and the fix for it won't be a line in a hosting migration plan.&lt;/p&gt;

&lt;p&gt;The pattern generalises past one repository. The project specified a control against a threat that&lt;br&gt;
sounded exactly right and had never been traced to the actual place the data flows through. It still&lt;br&gt;
held up real work, however briefly. And the thing worth guarding against the whole time was sitting&lt;br&gt;
in plain sight, in a directory nobody had thought to check, because the argument had already used up&lt;br&gt;
everyone's attention on the door that was never actually open.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://thekilted.dev/protected-nothing/" rel="noopener noreferrer"&gt;thekilted.dev/protected-nothing&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>buildinpublic</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Invoked, not executed</title>
      <dc:creator>the kilted dev</dc:creator>
      <pubDate>Thu, 20 Aug 2026 07:35:56 +0000</pubDate>
      <link>https://dev.to/thekilteddev/invoked-not-executed-1cg8</link>
      <guid>https://dev.to/thekilteddev/invoked-not-executed-1cg8</guid>
      <description>&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/BfC0iFzhFFs"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;A deep-research request to the top-tier model tore through three consecutive five-hour usage&lt;br&gt;
windows, the rolling quota Claude enforces before a session has to stop and reset, to answer a&lt;br&gt;
single question.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Usage window&lt;/th&gt;
&lt;th&gt;Burned&lt;/th&gt;
&lt;th&gt;Time to burn it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Window 1&lt;/td&gt;
&lt;td&gt;54%&lt;/td&gt;
&lt;td&gt;about two minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window 2&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;about four minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Window 3&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;finishing the job&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three consecutive five-hour usage windows against one question. That adds up to 194% - nearly two&lt;br&gt;
entire windows - and the first two were gone inside six minutes.&lt;/p&gt;

&lt;p&gt;Nobody had done anything unusual. Someone had just asked the model to look something up properly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://thekilted.dev/route-dont-guess/" rel="noopener noreferrer"&gt;The routing policy this machine runs on&lt;/a&gt; has a rule for&lt;br&gt;
exactly this shape of task: work that's one step (search, fetch a source, extract a claim) and&lt;br&gt;
objectively checkable belongs on the cheapest tier that can do it, not on the model that costs the&lt;br&gt;
most per token. Deep research is built entirely out of that shape: search, fetch, extract, feeding&lt;br&gt;
a synthesis step that does need judgment. The rule already existed. It just never had a chance to&lt;br&gt;
apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the rule never fired
&lt;/h2&gt;

&lt;p&gt;A routing policy only engages at a decision point: the moment a session has to choose which tier&lt;br&gt;
does the next piece of work. Most of the policy's machinery assumes that moment exists. A plan gets&lt;br&gt;
broken into tasks, and each task is a place to ask who does this.&lt;/p&gt;

&lt;p&gt;A direct request, "deep-research this," skips all of that. It's answered as one thing, by whichever&lt;br&gt;
model receives it, in whichever way that model has of doing research. There's no task list to route&lt;br&gt;
pieces of. The search, the fetching, the extraction, the verification all happen inside a single&lt;br&gt;
reply from the most expensive available model, because nothing ever asked whether the next step&lt;br&gt;
needed to be there.&lt;/p&gt;

&lt;p&gt;The fix follows from the gap. Route research-shaped requests through a harness that does the&lt;br&gt;
decomposing on the way in, whatever words were used to ask. That covers all three shapes the ask&lt;br&gt;
takes: an explicit "research this," a model deciding on its own that a claim needs checking, or&lt;br&gt;
something as soft as "I'm not sure what's actually true here." All three land in the same place if&lt;br&gt;
answered directly. All three should hit the harness instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test: 3 phrasings, 2 tiers
&lt;/h2&gt;

&lt;p&gt;Having independently found an adjacent problem (a third-party tool doing something similar had its&lt;br&gt;
own bug in the budget cap meant to prevent exactly this kind of burn), a sibling project was asked&lt;br&gt;
to check whether the routing fix triggered in practice. Six blind subagents, no shared context, one&lt;br&gt;
prompt each: three ways of asking for the same research, crossed with two model tiers. The topic&lt;br&gt;
was held constant, the enforcement status of the EU AI Act, so how the question was worded was the&lt;br&gt;
only deliberate variable.&lt;/p&gt;

&lt;p&gt;The intended way of measuring it failed. A subagent's own transcript file stays zero bytes even&lt;br&gt;
after the run has finished, so nobody could simply read off which tools had been called. The&lt;br&gt;
measurement fell back to asking each subject afterward what it had done, cross-checked against a&lt;br&gt;
tool-call count the harness reports and the agent has no hand in writing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The harness fired in half the runs. That looked like a pass. It wasn't one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What "invoked" was hiding
&lt;/h2&gt;

&lt;p&gt;Every run that triggered the harness received an instruction to call &lt;code&gt;Workflow&lt;/code&gt;, the dispatcher&lt;br&gt;
that fans the research out across sub-agents. &lt;code&gt;Workflow&lt;/code&gt; is not available inside a delegated&lt;br&gt;
subagent. Each of those runs found that out mid-task, noticed the gap, and quietly did the research&lt;br&gt;
a different way: by hand, with the tools it did have. Nothing crashed. Nothing logged an error. The&lt;br&gt;
final answer looked like a normal research answer, because it was one. Just not the one the rule&lt;br&gt;
was written to guarantee.&lt;/p&gt;

&lt;p&gt;One of the six was candid about exactly what was lost in translation. It had improvised its own&lt;br&gt;
version of the harness's fan-out step, sending several of its own sub-agents out to search in&lt;br&gt;
parallel, and reported the results converging as if that settled things. Asked afterward what its&lt;br&gt;
own five-agent agreement was worth, it corrected itself before anyone pushed back:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The convergence I reported between agents is corroboration, not adversarial verification, five&lt;br&gt;
agents drawing on an overlapping source pool can agree and still be wrong together.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The step that was skipped wasn't a decoration. It was the one part of the harness whose entire job&lt;br&gt;
is to catch exactly that kind of agreement that isn't evidence. A rule checking whether the harness&lt;br&gt;
was invoked would have called that run compliant. The harness had not, in the sense that mattered,&lt;br&gt;
run at all.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Harness invoked&lt;/th&gt;
&lt;th&gt;Fan-out executed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Explicit ask (sonnet)&lt;/td&gt;
&lt;td&gt;met&lt;/td&gt;
&lt;td&gt;broke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit ask (opus)&lt;/td&gt;
&lt;td&gt;met&lt;/td&gt;
&lt;td&gt;broke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No keyword (sonnet)&lt;/td&gt;
&lt;td&gt;broke&lt;/td&gt;
&lt;td&gt;never established&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No keyword (opus)&lt;/td&gt;
&lt;td&gt;broke&lt;/td&gt;
&lt;td&gt;never established&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft / vague (sonnet)&lt;/td&gt;
&lt;td&gt;met&lt;/td&gt;
&lt;td&gt;broke&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft / vague (opus)&lt;/td&gt;
&lt;td&gt;broke&lt;/td&gt;
&lt;td&gt;never established&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Invoked in three of six runs, executed in none. The three that invoked it reached for &lt;code&gt;Workflow&lt;/code&gt;,&lt;br&gt;
which a delegated subagent cannot call, and fell back to research by hand; the three that never&lt;br&gt;
invoked it never got far enough to find out. The right-hand column is empty either way, and that&lt;br&gt;
column is the one the rule was written to guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that looked best was the least trustworthy
&lt;/h2&gt;

&lt;p&gt;The one wording that worked reliably, the explicit "deep-research this," didn't work because the&lt;br&gt;
routing logic held. One of the successful runs said, unprompted, exactly why it had picked up on&lt;br&gt;
the request. The words read to it as plain English, and it matched them to the right tool on its&lt;br&gt;
own initiative. In its own account, the phrase was "hyphenated as a verb phrase," with "no slash,&lt;br&gt;
no backticks, no skill syntax."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nothing in how it was written signalled a tool call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Rename the tool, or ask for the same research without those two words, and the coincidence that&lt;br&gt;
made the easy case look solved stops holding.&lt;/p&gt;

&lt;p&gt;The case that matters is a serious, specific question asked with no research vocabulary in it. That&lt;br&gt;
is the shape almost every real research need takes, and it missed on every model tested. That's the&lt;br&gt;
gap worth worrying about.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed, and what didn't
&lt;/h2&gt;

&lt;p&gt;The rule was rewritten to check for evidence the harness had actually run: a count of sub-agents&lt;br&gt;
dispatched, a tally of claims independently verified, rather than trusting that the right words had&lt;br&gt;
been said. That closes the failure mode the test found: a compliant-looking run that quietly did&lt;br&gt;
none of the work the compliance was meant to certify.&lt;/p&gt;

&lt;p&gt;It doesn't close the other one. The wording that reliably works is still the wording that happens to&lt;br&gt;
match the tool's own name. The question that matters, the one with no research vocabulary in it,&lt;br&gt;
misses the harness on both models tried.&lt;/p&gt;

&lt;p&gt;Fixing what gets checked was the easy half. The other half is getting the harness to notice it is&lt;br&gt;
needed at all, from a question that never says so. That one is still open, and the coincidence that&lt;br&gt;
made the easy case look solved is the reason to expect it is the harder one.&lt;/p&gt;

&lt;p&gt;The rule that got rewritten, extracted and de-identified: &lt;a href="https://github.com/thekilteddev/routing-policy" rel="noopener noreferrer"&gt;the routing-policy&lt;br&gt;
repo&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>orchestration</category>
      <category>routing</category>
      <category>llmops</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>What really fits in 8GB VRAM</title>
      <dc:creator>the kilted dev</dc:creator>
      <pubDate>Tue, 18 Aug 2026 19:03:52 +0000</pubDate>
      <link>https://dev.to/thekilteddev/what-really-fits-in-8gb-vram-4o02</link>
      <guid>https://dev.to/thekilteddev/what-really-fits-in-8gb-vram-4o02</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Mstw8fKvjB0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Four VRAM numbers that looked wrong on an 8GB laptop, none of them a bug: what dxdiag actually&lt;br&gt;
reports, what the desktop holds back before a model even loads, and the two fit rules that came&lt;br&gt;
out of chasing all four down.&lt;/p&gt;

&lt;p&gt;Every question about running models locally reduces to one question: will it fit. Which model,&lt;br&gt;
which runtime, which quantisation, whether the thing is usable or a slideshow: all of it comes&lt;br&gt;
down to that one word. The trouble is that the number deciding the answer is never the number&lt;br&gt;
anything shows you.&lt;/p&gt;

&lt;p&gt;This is a laptop with an RTX 5060 and 8GB of VRAM. Four separate figures turned out to be&lt;br&gt;
measuring something adjacent to what actually mattered. Not one of them was wrong. Each was&lt;br&gt;
reporting a real quantity. Just not the quantity that decides whether a model loads.&lt;/p&gt;
&lt;h2&gt;
  
  
  The first number: shared memory
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;› &lt;span class="kd"&gt;dxdiag&lt;/span&gt; &lt;span class="na"&gt;/whql&lt;/span&gt;&lt;span class="nl"&gt;:off&lt;/span&gt; &lt;span class="na"&gt;/t &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;outfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Run that on this machine, open the Display tab, and it reports the graphics card as having&lt;br&gt;
&lt;strong&gt;24,144 MB&lt;/strong&gt; of display memory. Twenty-four gigabytes, on a laptop sold as having eight. Directly&lt;br&gt;
underneath, the same panel breaks it down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Display Memory:    24144 MB
Dedicated Memory:   7899 MB
Shared Memory:     16245 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7,899 plus 16,245 is 24,144. The arithmetic is honest. Display memory is the dedicated VRAM on the&lt;br&gt;
card plus a slice of system RAM the driver treats as overflow, half the machine's 31.7GB, the&lt;br&gt;
standard Windows allocation. Only the dedicated figure decides whether a model loads.&lt;/p&gt;

&lt;p&gt;Two details make the trap worse than a single mislabelled number. The shared pool isn't the&lt;br&gt;
graphics card's at all, and every display adapter on the machine claims it: the integrated Intel&lt;br&gt;
graphics, the RTX 5060, and two DisplayLink devices all report the same 16,245 MB. Add up what the&lt;br&gt;
machine appears to have and the total overstates the truth several times over. The dedicated&lt;br&gt;
figure isn't perfectly stable either: a model-fitting tool run on the same box reported 7.96GB&lt;br&gt;
where dxdiag says 7,899 MB, close enough to be the same answer, far enough apart that a fit&lt;br&gt;
calculation cutting it fine will disagree with itself depending on which tool it asked.&lt;/p&gt;

&lt;p&gt;The two pools behave completely differently under load. VRAM is fast and finite. The shared&lt;br&gt;
region is system RAM wearing a costume: spilling into it doesn't fail, it just gets slow, and a&lt;br&gt;
slowdown is worse than a failure because a failure tells you something is wrong while a slowdown&lt;br&gt;
just lets you keep going, wondering why everything feels off. A 9B model at 32K context sits&lt;br&gt;
entirely in VRAM at 5.9GB and generates 51.5 tokens/sec. Push the context to 64K and it needs&lt;br&gt;
7.5GB, spills to an 18/82 split, and drops to 22.6 tokens/sec. Same model, same machine, same&lt;br&gt;
quantisation. One setting, less than half the speed.&lt;/p&gt;

&lt;p&gt;None of this requires taking anyone's word for it: the breakdown is four lines down the Display&lt;br&gt;
tab on every Windows machine there is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second number: your desktop already has some of it
&lt;/h2&gt;

&lt;p&gt;The dedicated 7.9GB isn't yours either. Under normal use (browser open, editor open, the desktop&lt;br&gt;
doing its job) somewhere between 2 and 3.5GB is already spoken for before a single model loads.&lt;br&gt;
Not reserved in a way you can reclaim by closing one app. Just gone, in the ordinary course of&lt;br&gt;
using the computer for anything other than inference.&lt;/p&gt;

&lt;p&gt;The spread matters as much as the figure. Measured on this machine with a browser, a notes app, a&lt;br&gt;
spreadsheet and around thirty other things touching the GPU, it sits at almost exactly 2GB. On a&lt;br&gt;
busier day it has been half again that. On the quiet afternoon the benchmark below was recorded,&lt;br&gt;
it was under 1GB.&lt;/p&gt;

&lt;p&gt;That shows up in the benchmark record as an awkward footnote. Here is the same 36B&lt;br&gt;
mixture-of-experts model, three ways, every row measured at the same 32K context:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Gen t/s&lt;/th&gt;
&lt;th&gt;VRAM&lt;/th&gt;
&lt;th&gt;When you can use it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp, 8 expert layers on GPU&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;27.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.4GB&lt;/td&gt;
&lt;td&gt;Apps closed only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp, 6 expert layers on GPU&lt;/td&gt;
&lt;td&gt;26.3&lt;/td&gt;
&lt;td&gt;6.4GB&lt;/td&gt;
&lt;td&gt;Recorded safe default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama, automatic 80/20 CPU/GPU split&lt;/td&gt;
&lt;td&gt;23.4&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Default behaviour&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same day, same machine, flash attention on, KV cache quantised to q8_0. Ambient GPU load during&lt;br&gt;
measurement was unusually low, between 0.3 and 0.8GB; normal desktop load is around 3.5GB.&lt;/p&gt;

&lt;p&gt;That fastest row leaves around 800 MB of headroom, and it was measured on that unusually quiet&lt;br&gt;
afternoon. Under normal desktop load it doesn't fit at all. So the best number in the table is one&lt;br&gt;
that can't be reproduced while actually using the computer, and the benchmark file says so&lt;br&gt;
directly rather than quoting the headline figure and moving on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two rules that came out of it
&lt;/h2&gt;

&lt;p&gt;After enough of this, the fit question collapsed into two rules that are worth more than any&lt;br&gt;
individual measurement.&lt;/p&gt;

&lt;p&gt;Dense models: the file has to be about 7GB or smaller at 4-bit quantisation, and it has to run&lt;br&gt;
entirely on the GPU. If it spills, the speed lost is more than a bigger model would have gained.&lt;/p&gt;

&lt;p&gt;Mixture-of-experts models: the file has to fit in system RAM (31.7GB here) with the experts&lt;br&gt;
offloaded to the CPU. VRAM only needs to hold the attention layers and the key-value cache.&lt;/p&gt;

&lt;p&gt;The second rule produces the result that governs everything this project runs: a large&lt;br&gt;
mixture-of-experts model fits comfortably where a much smaller dense model doesn't. A 30B MoE&lt;br&gt;
model with a 19GB file runs at 32.9 tokens/sec generation on a 69/31 CPU/GPU split, at 16K&lt;br&gt;
context. A 14B dense model is simply ruled out, and so is the default pull of a well-known 9.6GB&lt;br&gt;
dense model, on the dense rule alone.&lt;/p&gt;

&lt;p&gt;That context qualifier isn't decoration. Speed figures are only comparable within a single&lt;br&gt;
context size, which is why every number here carries one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Parameter count isn't the constraint. Architecture is: a model three times the size fits because&lt;br&gt;
of how it's shaped.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's invisible from any spec sheet that lists parameters and file sizes. Not everything needs a&lt;br&gt;
hands-on trial to rule out, either. An 80B MoE model was ruled out at the research stage: its&lt;br&gt;
smallest 4-bit quantisation is 38.4GB, more than the machine has RAM for at all. No test required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The third number: the config that was right when it was written
&lt;/h2&gt;

&lt;p&gt;Six weeks in, a model that had been working for over a week stopped loading. Every attempt died&lt;br&gt;
with a CUDA out-of-memory error, and the first answer was wrong.&lt;/p&gt;

&lt;p&gt;The server log said it had offloaded 49 of 49 layers to the GPU, which reads unambiguously as "it&lt;br&gt;
tried to load the whole thing into VRAM instead of doing the CPU split that had been working." A&lt;br&gt;
satisfying scheduler bug, except it wasn't what happened. Reading the full memory breakdown&lt;br&gt;
instead of the first suspicious line showed the actual buffer allocation: 3.2 to 4GB in the GPU&lt;br&gt;
buffer against 13.5 to 14.3GB in pinned host memory. The CPU offload was correct the whole time.&lt;br&gt;
That log line's wording misled; it wasn't evidence.&lt;/p&gt;

&lt;p&gt;The real cause turned up in the runtime's own fit-calculation logging, and it wasn't a bug either.&lt;br&gt;
The fit calculation had correctly worked out that 43 of 49 layers would overflow to host RAM, and&lt;br&gt;
projected 5,123 MiB of VRAM use against 6,196 MiB free. A plausible-looking fit.&lt;/p&gt;

&lt;p&gt;What had actually happened was that the model's local alias had been silently recreated eight&lt;br&gt;
days earlier, inheriting the project's default context setting of 32,768, and this particular&lt;br&gt;
model had never been verified at that context. It had been benchmarked at 16K.&lt;/p&gt;

&lt;p&gt;The evidence was sitting in the original benchmark file the whole time, contradicting itself. The&lt;br&gt;
prose describing the working run states the context parameter as 32,768. The speed table further&lt;br&gt;
down labels the identical CPU/GPU split "@16K." Both written the same day, about the same run.&lt;br&gt;
Nobody reads a benchmark file for internal consistency after writing it, so the contradiction sat&lt;br&gt;
there quietly. A single-variable test settled it: at 16,384 the model loads and generates every&lt;br&gt;
time; at 32,768 it fails every time. The fix was re-pinning the alias: a configuration&lt;br&gt;
correction, not a code change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fourth number: free memory doesn't predict this
&lt;/h2&gt;

&lt;p&gt;The most useful part of that failure is the bit that contradicts intuition. The out-of-memory&lt;br&gt;
error didn't depend on how much VRAM was free. Retested with the model fully unloaded and 6.1GB&lt;br&gt;
available, it still failed, ruling out the obvious explanation that some other app was&lt;br&gt;
transiently holding memory. At 32K context, the model's key-value cache plus its compute buffers&lt;br&gt;
exceed what fits after the CPU offload split, regardless of what's free. Headroom isn't the&lt;br&gt;
variable. The shape of the allocation is.&lt;/p&gt;

&lt;p&gt;That has a practical edge: a fit-estimating tool used on this machine calculates against total&lt;br&gt;
VRAM rather than free VRAM, so its recommendations need hand-adjusting for whatever the desktop is&lt;br&gt;
already holding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this adds up to
&lt;/h2&gt;

&lt;p&gt;Four numbers, four different ways of being adjacent to the truth. Windows reports memory that&lt;br&gt;
includes a region you don't want to touch. The desktop silently holds a few gigabytes of what&lt;br&gt;
remains. A log line describes full GPU offload while performing a CPU split. A config file records&lt;br&gt;
a context the model was never tested at, and a fitting tool measures against a total nobody&lt;br&gt;
actually has.&lt;/p&gt;

&lt;p&gt;None of these is a bug. Every one is a real measurement of something, presented plainly, by&lt;br&gt;
software with no intention to mislead. They're wrong only in the sense that they answer a question&lt;br&gt;
next to the one being asked, which is why the two fit rules are worth more than the measurements&lt;br&gt;
that produced them. The rules survive a driver update, a runtime version bump, a model release.&lt;br&gt;
The numbers don't, and the ones that look most authoritative are usually the ones measured on an&lt;br&gt;
unusually quiet afternoon.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://thekilted.dev/what-really-fits-in-8gb-vram/" rel="noopener noreferrer"&gt;thekilted.dev/what-really-fits-in-8gb-vram&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>localllm</category>
      <category>vram</category>
      <category>gpu</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Nine ways to talk to a local model</title>
      <dc:creator>the kilted dev</dc:creator>
      <pubDate>Tue, 11 Aug 2026 15:56:51 +0000</pubDate>
      <link>https://dev.to/thekilteddev/nine-ways-to-talk-to-a-local-model-ocj</link>
      <guid>https://dev.to/thekilteddev/nine-ways-to-talk-to-a-local-model-ocj</guid>
      <description>&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/nrKHclXh4a0"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;A nine-tool survey of local-model interfaces on one GPU: what worked, what silently failed, and&lt;br&gt;
why what sits between you and the model matters more than which model you picked.&lt;/p&gt;

&lt;p&gt;Nine pieces of software for talking to a local model went through this machine over about two&lt;br&gt;
weeks. Same GPU, largely the same handful of models, one afternoon each.&lt;/p&gt;

&lt;p&gt;The spread in outcomes was enormous, and almost none of it was about the model. The same weights&lt;br&gt;
that scored full marks through one harness produced unparseable garbage through another, and hung&lt;br&gt;
indefinitely through a third. What sits between you and the model turns out to matter more than&lt;br&gt;
which model you picked, which is a boring conclusion until you notice it also means most model&lt;br&gt;
comparisons are measuring something else.&lt;/p&gt;

&lt;p&gt;Here is what each one was actually good at.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Good at&lt;/th&gt;
&lt;th&gt;The number that mattered&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;the default, broadly reliable&lt;/td&gt;
&lt;td&gt;32.9 t/s gen on a 30B MoE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama.cpp&lt;/td&gt;
&lt;td&gt;fastest, once tuned&lt;/td&gt;
&lt;td&gt;27.1 t/s vs Ollama's 23.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LM Studio&lt;/td&gt;
&lt;td&gt;document extraction to file&lt;/td&gt;
&lt;td&gt;10 rows, exact match, no invented values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;little-coder&lt;/td&gt;
&lt;td&gt;small-model coding harness&lt;/td&gt;
&lt;td&gt;8/8 in 2.1 min, then a version bump broke it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsloth Studio&lt;/td&gt;
&lt;td&gt;fastest single-file result&lt;/td&gt;
&lt;td&gt;unusable past a 4096-token ceiling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Goose&lt;/td&gt;
&lt;td&gt;agent framework, right idea&lt;/td&gt;
&lt;td&gt;30% GPU vs 98%, one setting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code as client&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;all three connection methods fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Odysseus&lt;/td&gt;
&lt;td&gt;the most polished interface&lt;/td&gt;
&lt;td&gt;0/8, only one of three files actually saved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continue.dev&lt;/td&gt;
&lt;td&gt;autocomplete, nothing else does it&lt;/td&gt;
&lt;td&gt;355MB free at 32K, no room for anything else&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Ollama, the one everything else is measured against
&lt;/h2&gt;

&lt;p&gt;The default answer, and it stayed the default. It runs a 30B mixture-of-experts model at 32.9&lt;br&gt;
tokens/sec generation and 443 tokens/sec prefill on a 69/31 CPU/GPU split, which is the single&lt;br&gt;
most useful capability on this box.&lt;/p&gt;

&lt;p&gt;Two things about it are worth knowing before you trust it. Its desktop application stores a&lt;br&gt;
context-length setting in a local database that &lt;strong&gt;overrides the environment variable&lt;/strong&gt; you set,&lt;br&gt;
which produced a memorable afternoon of a model ignoring configuration that was demonstrably&lt;br&gt;
correct. And model aliases pin their own context, which is how a working alias silently acquired&lt;br&gt;
a context it had never been tested at and started failing weeks later.&lt;/p&gt;

&lt;p&gt;Neither is a defect exactly. Both are the kind of thing you only learn by being caught by them.&lt;/p&gt;

&lt;p&gt;More: &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;ollama.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  llama.cpp, faster if you're willing to tune it
&lt;/h2&gt;

&lt;p&gt;For one specific job, a 36B mixture-of-experts model at 32K context, llama.cpp beats Ollama by&lt;br&gt;
about 16% on generation and 34% on prefill. 27.1 tokens/sec against 23.4.&lt;/p&gt;

&lt;p&gt;That margin isn't free. It came from three separate tuning discoveries: disabling memory-mapped&lt;br&gt;
file loading, which llama.cpp itself warns is slow when combined with CPU tensor overrides and&lt;br&gt;
which alone accounted for most of the gap; setting thread count to the CPU's fourteen physical&lt;br&gt;
cores rather than the eight the script had; and tuning how many expert layers stay on the GPU.&lt;/p&gt;

&lt;p&gt;Untuned, the same path runs at 13 tokens/sec, less than half. The default configuration of the&lt;br&gt;
faster runtime is slower than the alternative, which is worth remembering whenever a benchmark&lt;br&gt;
reports that one tool beats another.&lt;/p&gt;

&lt;p&gt;llama.cpp's server also has the most complete tool-calling story of anything tested here. Given a&lt;br&gt;
toy arithmetic tool, it emitted five valid parseable calls out of five. Given three near-identical&lt;br&gt;
tools and six questions, it selected correctly six times out of six. That second number is the one&lt;br&gt;
that matters, because choosing between similar tools is where tool-calling usually breaks.&lt;/p&gt;

&lt;p&gt;It also has server-side built-in tools, off by default, with the server itself warning against&lt;br&gt;
exposing them to untrusted environments (the available set includes shell execution). Only the&lt;br&gt;
read-only subset was enabled here. Notably, the server does not auto-execute anything: it returns&lt;br&gt;
the tool call and the client drives the loop. That's the right design, and it means the safety&lt;br&gt;
boundary sits where you can see it.&lt;/p&gt;

&lt;p&gt;More: &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;github.com/ggml-org/llama.cpp&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  LM Studio, the graphical one, and the absolute-path rule
&lt;/h2&gt;

&lt;p&gt;Good at exactly one thing this project needed: read some documents, extract structured data, write&lt;br&gt;
the result to a file. Given real utility bills it produced ten rows across two months with every&lt;br&gt;
usage quantity and cost matching the source exactly. No invented values.&lt;/p&gt;

&lt;p&gt;Getting there took three attempts. The first two failed with filesystem permission errors even&lt;br&gt;
after access was explicitly approved through its own dialog. The difference was the prompt: "the&lt;br&gt;
files in your workspace" failed, and naming the absolute directory worked.&lt;/p&gt;

&lt;p&gt;Whether the model requested bad relative paths or the permission scope failed to resolve them was&lt;br&gt;
never determined. The rule is empirical and it's reliable: give it absolute paths.&lt;/p&gt;

&lt;p&gt;More: &lt;a href="https://lmstudio.ai" rel="noopener noreferrer"&gt;lmstudio.ai&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  little-coder, the one that proved the whole thing was possible
&lt;/h2&gt;

&lt;p&gt;A thin harness tuned for small models, giving them real file-writing tools rather than asking them&lt;br&gt;
to print code into a chat window. It's what scored 8 out of 8 on this project's benchmark in 2.1&lt;br&gt;
minutes, including the follow-up-fix-without-regression test that had defeated everything before&lt;br&gt;
it.&lt;/p&gt;

&lt;p&gt;Then a version bump broke it. With its default model it began emitting raw XML that nothing&lt;br&gt;
parsed, exiting successfully, and writing no files, the worst failure shape available, because an&lt;br&gt;
exit code of zero and no error output reads as success to anything automated.&lt;/p&gt;

&lt;p&gt;The instructive part is the diagnosis, which was wrong the first time. The failure was recorded as&lt;br&gt;
being caused by the version change breaking model-id resolution, because a warning about the model&lt;br&gt;
id appeared next to the failure. A later controlled test held the model constant and varied only&lt;br&gt;
id registration: the warning is benign, and the actual fault is the specific model. The same&lt;br&gt;
harness works with a different one.&lt;/p&gt;

&lt;p&gt;Adjacency isn't causation, and a warning that appears next to a failure is still just a warning&lt;br&gt;
that appears next to a failure.&lt;/p&gt;

&lt;p&gt;More: &lt;a href="https://github.com/itayinbarr/little-coder" rel="noopener noreferrer"&gt;github.com/itayinbarr/little-coder&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Unsloth Studio, fastest and unusable
&lt;/h2&gt;

&lt;p&gt;The fastest single-file result of anything tested: ten seconds.&lt;/p&gt;

&lt;p&gt;Its context window control is broken on Windows. The effective ceiling is 4096 tokens regardless&lt;br&gt;
of what the interface is set to, which makes multi-file work impossible and makes any measurement&lt;br&gt;
taken through it incomparable to anything else. A tool that silently runs at a fraction of the&lt;br&gt;
context you configured is worse than one that refuses, because the number it shows you is a number&lt;br&gt;
you will use.&lt;/p&gt;

&lt;p&gt;It also gets flagged as malware by the antivirus on this machine, which is a false positive, and&lt;br&gt;
which is its own small saga.&lt;/p&gt;

&lt;p&gt;More: &lt;a href="https://unsloth.ai" rel="noopener noreferrer"&gt;unsloth.ai&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Goose, right idea, wrong backend
&lt;/h2&gt;

&lt;p&gt;Goose works here, with one hard rule: use its Ollama provider, never its built-in llama.cpp.&lt;/p&gt;

&lt;p&gt;The built-in path manages roughly 30% GPU and 25% CPU utilisation on Windows, a broken offload&lt;br&gt;
that leaves the hardware idle while the model crawls. Through the Ollama provider the same machine&lt;br&gt;
runs at 98% GPU.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Same tool, same model, same box. One configuration choice, and the difference is the entire&lt;br&gt;
value of having a GPU.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;More: &lt;a href="https://github.com/block/goose" rel="noopener noreferrer"&gt;github.com/block/goose&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code itself, three methods, one root cause
&lt;/h2&gt;

&lt;p&gt;The obvious idea, and the one people ask about most: point the agentic coding tool you already use&lt;br&gt;
at a local model, and stop paying for tokens.&lt;/p&gt;

&lt;p&gt;Three connection methods were tested. Setting the API base URL directly, which produces no output&lt;br&gt;
without also setting an auth token to a dummy value. A proxy shim, which worked and ran at roughly&lt;br&gt;
two and a half minutes per response on CPU offload. And the runtime's own native launch command,&lt;br&gt;
which produced a model hallucinating unrelated tasks and emitting raw tool-call syntax as text.&lt;/p&gt;

&lt;p&gt;All three fail, and they fail for the same reason: &lt;strong&gt;local models do not parse Claude Code's&lt;br&gt;
system prompt format.&lt;/strong&gt; That's not a configuration problem, and no amount of trying a fourth&lt;br&gt;
connection method changes it. The finding was worth writing down precisely so nobody here burns&lt;br&gt;
another afternoon on connection method five.&lt;/p&gt;

&lt;p&gt;More: &lt;a href="https://claude.com/product/claude-code" rel="noopener noreferrer"&gt;claude.com/product/claude-code&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Odysseus, the most polished tool tested
&lt;/h2&gt;

&lt;p&gt;The most polished thing tested, and the worst result.&lt;/p&gt;

&lt;p&gt;It scored 0 out of 8 on the same benchmark. The run climbed from 0.74 to 10.19 tokens/sec, then&lt;br&gt;
stalled dead at "4% complete" with a byte-identical transcript across a five-minute recheck.&lt;br&gt;
Underneath, the backend had dropped the generation stream and was returning 404 to the frontend's&lt;br&gt;
status polls, which the frontend kept making, forever, with no error surfaced, no retry, and no&lt;br&gt;
timeout. A silent, indefinite hang.&lt;/p&gt;

&lt;p&gt;Two things compounded it. Its interface exposes no control for disabling thinking mode, which this&lt;br&gt;
project has needed since day one, so the model visibly re-planned and restarted its own draft&lt;br&gt;
mid-stream. And checking its own document panel rather than trusting the chat transcript revealed&lt;br&gt;
that &lt;strong&gt;only one of three files had actually been saved.&lt;/strong&gt; The transcript showed content that was&lt;br&gt;
never written anywhere.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A chat transcript is a record of what a model said, not what a system did, and those two things&lt;br&gt;
diverge silently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's also structurally unable to do what this project needs: its tools are a fixed set of built-in&lt;br&gt;
capabilities toggled per message, not a registry you can register an arbitrary function with.&lt;br&gt;
There was no way to run the tool-calling probe at all. Not a bug, a different product.&lt;/p&gt;

&lt;p&gt;More: &lt;a href="https://github.com/odysseus-dev/odysseus" rel="noopener noreferrer"&gt;github.com/odysseus-dev/odysseus&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Continue.dev, the one that does something nothing else does
&lt;/h2&gt;

&lt;p&gt;An editor extension, evaluated for autocomplete rather than chat, because autocomplete is the one&lt;br&gt;
capability nothing else here provides.&lt;/p&gt;

&lt;p&gt;A 4B model completes a fill-in-the-middle hole in 209 to 252 milliseconds at 127 tokens/sec,&lt;br&gt;
comfortably inside the budget where ghost text feels helpful rather than laggy. A 9B does it in&lt;br&gt;
around 328 milliseconds. A 14B coder model, the one nominally built for this, takes 2.5 seconds,&lt;br&gt;
because it spills to CPU, and rambles past the hole into inventing further functions.&lt;/p&gt;

&lt;p&gt;The measurement that mattered was not speed, though. With the autocomplete model warm at 32K&lt;br&gt;
context, free video memory drops to about 355MB for the 9B and 1GB for the 4B. &lt;strong&gt;There is no room&lt;br&gt;
for anything else.&lt;/strong&gt; You can have interactive coding with autocomplete, or a background delegation&lt;br&gt;
run. Not both.&lt;/p&gt;

&lt;p&gt;More: &lt;a href="https://continue.dev" rel="noopener noreferrer"&gt;continue.dev&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison we never ran
&lt;/h2&gt;

&lt;p&gt;All of this was supposed to be settled by a formal benchmark. It was written into the handover&lt;br&gt;
document that started this project, as open item one: the word search task run across five&lt;br&gt;
entrants, Goose's built-in backend, Goose via Ollama, llama.cpp, Unsloth Studio and LM Studio,&lt;br&gt;
against one baseline model, with context standardised at 89K on every runtime so the results would&lt;br&gt;
be comparable.&lt;/p&gt;

&lt;p&gt;The plan even predicted its own casualty:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Unsloth will fail this, document it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was closed without ever being run.&lt;/p&gt;

&lt;p&gt;Not from lack of time. By the time it came up for scheduling, its actual question, which runtime&lt;br&gt;
for which job, had already been answered by using them. Ollama for the coding harness. llama.cpp&lt;br&gt;
for the 36B at 32K. LM Studio for graphical document extraction. Goose only via Ollama. Unsloth&lt;br&gt;
ruled out on the context ceiling. The proxy shim never a peer to the others, because it's a&lt;br&gt;
different category of thing entirely.&lt;/p&gt;

&lt;p&gt;A generic five-way bake-off would have re-measured memory-spill behaviour already understood, at a&lt;br&gt;
context size chosen to be fair rather than useful, and changed not one of those decisions. The&lt;br&gt;
tools had already differentiated themselves on the only axis that mattered: what happened when&lt;br&gt;
real work went through them.&lt;/p&gt;

&lt;p&gt;One footnote, found while checking the above against the original plan rather than against our own&lt;br&gt;
notes. The record written when the benchmark was closed describes it as having six entrants, and&lt;br&gt;
lists a different set, merging Goose's two configurations into one, adding a runtime the plan had&lt;br&gt;
not included, and adding a proxy shim the plan mentions only in its list of things already&lt;br&gt;
rejected. The reasoning for closing it was sound and every per-use-case answer holds. The entrant&lt;br&gt;
list was simply restated from memory rather than reread, in a note written the same week the plan&lt;br&gt;
was still open.&lt;/p&gt;

&lt;p&gt;A benchmark nobody ran is a low-stakes thing to miscount. It's the same move that makes a&lt;br&gt;
miscounted one dangerous.&lt;/p&gt;

&lt;p&gt;The rule that came out of it is narrow. Benchmark a use case, never a category. If a new runtime&lt;br&gt;
appears, or a genuinely new job appears, measure &lt;em&gt;that&lt;/em&gt;, and if the answer is already visible in&lt;br&gt;
work you have done, the benchmark is a formality you are performing for the shape of it.&lt;/p&gt;

&lt;p&gt;Nine tools. The differences that mattered were reliability, offload behaviour, honesty about what&lt;br&gt;
was actually written to disk, and whether the thing exposed the one control the model needed. None&lt;br&gt;
of that shows up in a table of tokens per second.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://thekilted.dev/nine-ways-to-talk-to-a-local-model/" rel="noopener noreferrer"&gt;thekilted.dev/nine-ways-to-talk-to-a-local-model&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>localllm</category>
      <category>ollama</category>
      <category>llamacpp</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Convergent evidence</title>
      <dc:creator>the kilted dev</dc:creator>
      <pubDate>Mon, 10 Aug 2026 18:57:13 +0000</pubDate>
      <link>https://dev.to/thekilteddev/convergent-evidence-3ohk</link>
      <guid>https://dev.to/thekilteddev/convergent-evidence-3ohk</guid>
      <description>&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/iaFH_sg8mec"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;Companion to &lt;a href="https://thekilted.dev/route-dont-guess/" rel="noopener noreferrer"&gt;Route, don't guess&lt;/a&gt;: nine tools crossed the desk during the ten-day build. Not one became a dependency. The reasons why are the actual argument for building it in-house.&lt;/p&gt;

&lt;p&gt;Building something in-house invites an obvious question: did you check whether someone already shipped it? A companion project, the one that tracks and evaluates AI tooling for the whole machine, kept an eye out for exactly that, every time an orchestration-shaped tool crossed its desk. The bar it held every candidate to was narrow on purpose: does it add a capability the orchestrator doesn't already have?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Almost nothing cleared it. That's less interesting as a verdict than as a pattern.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The direct comparators
&lt;/h2&gt;

&lt;p&gt;Six candidates were orchestrators or multi-agent harnesses solving the same problem: which model does which task, and how do you trust what comes back.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fable Foreman&lt;/strong&gt; is parked, worth watching. It sits near one-to-one with our own routing policy: capability-class tiers, blind verification, budget-aware dispatch. The value was design comparison rather than new capability. It was also four days old at review, with two of its three headline features gated behind a call-to-action and never actually shown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruflo&lt;/strong&gt; went to the backlog. A hundred-plus prebuilt agents, vector memory, multi-model routing, some 210 tool integrations, 58k+ GitHub stars and climbing. What it sells is the routing-plus-verification loop already running here, more simply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CrewAI skills&lt;/strong&gt; was rejected on overlap, not quality. It's official plugin scaffolding for an orchestrator, researcher, writer, and reviewer crew, from a legitimate 55.5k-star parent project. The same pattern already runs on the machine's own workflow and routing layers; a third-party framework would duplicate it rather than extend it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9router&lt;/strong&gt; was rejected hard. It's a credential-interception proxy for every model provider, carrying an unauthenticated remote-code-execution flaw scored CVSS 10, plus terms-of-service risk for any subscription routed through it. An existing fallback covers the same need safely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Council&lt;/strong&gt; was declined by choice. A genuine multi-model consultation plugin, honestly framed, but a lighter version of a panel-of-models pattern already covered elsewhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three "agentic OS" reels&lt;/strong&gt; were rejected with nothing to install. Camera mockups of an always-on multi-agent life OS. Every named part (a constitution file, tier routing, adversarial sparring, a cost ledger, a budget halt) maps onto pieces already running here, separately and more safely. The bundle is marketing wrapped around five patterns already decomposed on purpose.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The adjacent category
&lt;/h2&gt;

&lt;p&gt;The second cluster doesn't route anything. These are desktop shells for running a fan-out once you've decided on one, checked for an unrelated reason rather than because they compete with the routing policy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orca&lt;/strong&gt; was confirmed hands-on. Free, runs the real CLI under your own subscription; skill and plugin parity held up, and a worktree fan-out plus diff-and-merge round-trip verified end to end on a scratch repo. 12.1k stars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nimbalyst&lt;/strong&gt; went to the backlog. Kanban-style parallel sessions with worktree isolation, parked pending confirmation it plays well with an existing hook-based workflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevSwarm&lt;/strong&gt; was rejected. Same category as Orca, but closed-source, paid, download-only, and the public repo is a landing page with 87 stars. It doesn't clear a bar a free alternative already cleared.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What cleared the bar wasn't a tool
&lt;/h2&gt;

&lt;p&gt;Two inputs cleared the bar, both as ideas rather than installs, arriving from opposite directions and landing on the same shape.&lt;/p&gt;

&lt;p&gt;A stranger's public routing config contributed one genuinely new distinction: taste as an axis separate from difficulty. A component name or a marketing hook is trivial to compute, but still wants the highest-taste model available. A difficulty-only rule would route that down, wrongly. The axis is now a permanent exception in the routing policy. The same source's "manager never runs high reasoning effort" rule was weighed and rejected; it conflicts directly with keeping the terminal review and hardest verification stages deliberately expensive.&lt;/p&gt;

&lt;p&gt;And an academic paper on filesystem-as-agent-architecture (numbered stage folders, a per-stage context file, human gates between them) turned out to overlap about 80% with the four-file working-notes system already in use. Independent convergence, this time from arXiv instead of a product pitch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neither produced a dependency. Both produced a stronger version of something that already existed.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The closest comparator, side by side
&lt;/h2&gt;

&lt;p&gt;Fable Foreman is worth lining up in full, precisely because it's the closest external thing to the in-house build.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Fable Foreman&lt;/th&gt;
&lt;th&gt;The in-house orchestrator&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Routing basis&lt;/td&gt;
&lt;td&gt;Capability class: frontier / workhorse / fast&lt;/td&gt;
&lt;td&gt;Verifiability + iteration count, down a cost ladder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model naming&lt;/td&gt;
&lt;td&gt;Aliases resolved at runtime&lt;/td&gt;
&lt;td&gt;Same. Tier by role, never a hardcoded model ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification&lt;/td&gt;
&lt;td&gt;"Trusts no one": blind verify against the real workload&lt;/td&gt;
&lt;td&gt;Same posture. Never trust a subagent's "done" without evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Budget awareness&lt;/td&gt;
&lt;td&gt;Budget-aware dispatch, degradation rule&lt;/td&gt;
&lt;td&gt;Append-only ledger plus a budget halt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design/review routing&lt;/td&gt;
&lt;td&gt;Not specified&lt;/td&gt;
&lt;td&gt;Never routes down. A standing, explicit rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust surface&lt;/td&gt;
&lt;td&gt;Four days old, spawns full agent fleets, content gated behind a CTA&lt;/td&gt;
&lt;td&gt;In-house, auditable, every piece proven separately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The market kept independently rediscovering the same routing model: a stranger's config file, an academic paper, three viral reels, and a four-day-old repo that maps onto our own build dimension for dimension. None became a dependency, because the same five patterns were already decomposed into separable, auditable pieces before any of them showed up.&lt;/p&gt;

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

&lt;p&gt;A single rejected tool proves nothing; every project rejects things. What's worth noticing is the shape of nine candidates checked and not one dependency adopted. Not one offered a capability the routing layer didn't already have, and the two that offered something real turned out to be ideas rather than products. That's what you'd expect if the underlying problem (route by verifiability, verify blindly, cap the budget, keep a human in the loop) is well-understood enough that independent people keep arriving at the same four or five pieces.&lt;/p&gt;

&lt;p&gt;The discipline wasn't refusing to look. It was refusing to trade five pieces we can audit for one box we can't. This doesn't claim there were no more tools worth checking. Only that everything that crossed the desk was checked against the same bar, and the bar held.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://thekilted.dev/convergent-evidence/" rel="noopener noreferrer"&gt;thekilted.dev/convergent-evidence&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>buildinpublic</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Route, don't guess</title>
      <dc:creator>the kilted dev</dc:creator>
      <pubDate>Mon, 10 Aug 2026 15:20:58 +0000</pubDate>
      <link>https://dev.to/thekilteddev/route-dont-guess-52gg</link>
      <guid>https://dev.to/thekilteddev/route-dont-guess-52gg</guid>
      <description>&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/fhYRAGDkRTE"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;Field notes from the machine: how we built the orchestrator that routes each task to the optimal model, and the near miss that drew the line on what it's allowed to act on.&lt;/p&gt;

&lt;p&gt;Every Claude Code session defaults to the model it was opened with. Ask it to rename a variable or design a whole subsystem and, left alone, it reaches for the same tier both times. That's fine until the quota notices. Tokens and time are one budget, and the top tier spends both fastest. Waste them on a variable rename and they aren't there for the subsystem. The obvious fix is a rule of thumb: low-cost model for easy stuff, expensive model for hard stuff. It sounds right. It's wrong, and working out why it's wrong is most of what this project turned out to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually decides
&lt;/h2&gt;

&lt;p&gt;The orchestrator is a routing layer, not another agent framework. It lives as markdown: a contract file, a handful of dispatch templates, an append-only ledger, all read by a session before it delegates anything. No installed dependency, no external scheduler. The bet, made on day one, was that Claude Code's own primitives were already enough, and the missing piece was a policy rather than a platform.&lt;/p&gt;

&lt;p&gt;The policy's core move is refusing "hard versus easy" as the routing variable. What matters is task shape: how many correction rounds the work needs, and whether a given round can be checked objectively.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route down&lt;/strong&gt; — one-shot, objectively checkable: tests exist, or the output validates against a schema. Push it down the cost ladder, through scripts and local models before the low-cost hosted tiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stays top&lt;/strong&gt; — iterative and judgment-heavy: design, debugging, architecture, review. Stays with the top-tier model, in-session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Underneath that split sit three costs the "low-cost tier for easy tasks" instinct never prices in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold-start tax&lt;/strong&gt; — every delegated session re-derives context from nothing, so handing off a five-minute edit loses outright.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review tax&lt;/strong&gt; — whoever dispatched the work still pays to read whatever comes back, and a chatty response from a low-cost tier can erase its own savings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry tax (dominant)&lt;/strong&gt; — low-cost models fail more often at anything with judgment in it, and every failure means a redo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule that survives all three isn't "is this task hard". It's whether the expected saving beats the expected cost of doing it again. Iteration-heavy work maximises the wrong side of that inequality no matter how easy it looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What never routes down
&lt;/h2&gt;

&lt;p&gt;Four categories are pinned to the top tier permanently, however simple the task looks in isolation.&lt;/p&gt;

&lt;p&gt;Taste-sensitive output is the first: design, copy, naming, brand voice. A component name is trivial to compute and still wants the model with taste. It turns out "easy" and "low-stakes" are different axes, an idea that arrived from outside the project and earned its place.&lt;/p&gt;

&lt;p&gt;The terminal review is the second. Per-task reviews can run on a mid tier, but the whole-branch pass before anything ships always runs top-tier, because it catches what the narrower passes miss. A pass on a low-cost tier once sailed past a real security hole that a stronger model then caught. The rule dates from that day.&lt;/p&gt;

&lt;p&gt;Third, a green light from a low-cost tier gates a dispatch but never substitutes for verification. Low-cost models have a structural habit of reporting success they haven't earned.&lt;/p&gt;

&lt;p&gt;Fourth, the orchestration layer itself. A low-cost model was tried as middle manager between the session and a worker. Sixty minutes and roughly fifty tool calls later it had shipped nothing and quietly run a global install nobody asked for. Delegation stays a command the session issues directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;› &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"nobody gets a middle manager"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Proof over promises
&lt;/h2&gt;

&lt;p&gt;Every delegated run ends the same way: a status block in the chat, and a row appended to a ledger file. Neither trusts a bare claim. A "done" needs the tool output, test result, or screenshot sitting next to it.&lt;/p&gt;

&lt;p&gt;The ledger isn't decoration. Each row records what was routed where, whether it passed, and the field that matters most: any lesson. A populated lesson is a candidate for promotion into the routing rules themselves. The policy tightens from its own field evidence, not just from design sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The near miss
&lt;/h2&gt;

&lt;p&gt;The hardest lesson didn't come from a cost overrun. It came from a decision that would have run correctly, in the wrong place.&lt;/p&gt;

&lt;p&gt;A "do it everywhere at once" clause, written into one project's own decision log, would have had that project's session run &lt;code&gt;git rm --cached&lt;/code&gt; against a &lt;em&gt;sibling&lt;/em&gt; project's repository, one it had no business touching. The user caught it before it executed. Nothing was lost.&lt;/p&gt;

&lt;p&gt;But it exposed the real default across every project on the machine: any session could offer to edit a neighbouring project's files, and the offer usually looked reasonable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix wasn't a smarter check. It was inverting the default.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A session now treats every other project's tree in three bands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read&lt;/strong&gt; (always allowed) — reading a sibling's files is always allowed, since no desync is possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Propose&lt;/strong&gt; (the only sanctioned cross-project write) — writing a note into the sibling's root asking its own next session to decide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutate&lt;/strong&gt; (denied by default) — editing a sibling's facts or running commands in its repo. Two narrow carve-outs: files whose whole purpose is to receive writes, like the routing ledger, and gated edits to genuinely shared config, which always name their blast radius and wait for sign-off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else routes through a proposal note. It suggests; it never asserts. The receiving project's own next session folds it into its own files, then deletes it. No queue, no index, no daemon. Three ad hoc uses proved the pattern before anyone wrote it down as a rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it came together
&lt;/h2&gt;

&lt;p&gt;Eleven days, roughly a session a day, each one closing with the same four working files (notes, memory, active constraints, rejected approaches) so the next session opens cold and still knows why past decisions landed where they did.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 1 — Founded on native primitives.&lt;/strong&gt; External orchestration frameworks rejected in favour of Claude Code's own agent files, skills, and hooks. First trial ran in a throwaway project on purpose, for zero regression risk while the design was unproven.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 2 — The ladder tested for real.&lt;/strong&gt; A wiki migration ran as the first live trial, with a script, a local model, and two hosted tiers all doing genuine work in one pipeline. The layered-review rule landed that same week, after a low-cost pass missed that security hole.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~Same week — First shipped consumer.&lt;/strong&gt; A pension calculator's feature became the first real work routed end-to-end through the policy. It surfaced the first known gap: the local tier only speaks Python, and this task was JavaScript. Not a misroute, a disqualification, but a gap worth naming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 4 — Outside validation.&lt;/strong&gt; An unrelated public routing framework, reviewed independently, converged on the same core shape from a completely different workload. We folded in its one genuinely new idea: taste as a routing axis distinct from difficulty. We rejected its "never run high reasoning effort" rule, since it conflicts with exactly the stages this project keeps expensive on purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 6 — Cross-project messaging formalised.&lt;/strong&gt; Three ad hoc successes generalised into the proposal-note convention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 7 — The near miss, and the contract it produced.&lt;/strong&gt; Default-deny on cross-project mutation became a standing, machine-wide rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 10 — Fleet-wide rollout.&lt;/strong&gt; The enforcement line, the one that makes a project's session actually consult the policy instead of silently defaulting, was offered across around eighteen sibling projects. Opt-in per project, tracked from the filesystem alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 11 — The ledger checks itself.&lt;/strong&gt; A staleness audit found most adopted projects had quietly stopped logging, so a hook now nudges emission at session end. The same day, a reviewer on a low-cost tier blamed an unrelated, pre-existing file change on the task it was checking. Comparing timestamps caught it, and it became a standing rule: a low-cost tier may scan, but attributing a found change to a cause needs a timestamp check first, not a guess.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's still open
&lt;/h2&gt;

&lt;p&gt;Three gaps stay visible on purpose. The local tier only routes Python-shaped work, so JavaScript tasks run top-tier by default until a second real JS consumer justifies building a driver. Reasoning effort never entered the model at all: the policy routes on which tier runs a task, not on how hard that tier is told to think, and the same model at a different effort setting is a different cost and arguably a different capability. That dimension is unexamined, not settled. And one mid-tier code-generation route sits in the ladder unproven: named, priced, ready, and untouched, because no task has gone through it yet.&lt;/p&gt;

&lt;p&gt;A rung nobody has used is not the same as a rung that works.&lt;/p&gt;

&lt;p&gt;Nothing above is theory. Every rule came from work that actually ran. The whole apparatus is a bet that the interesting problem was never "which model is smartest". It was knowing, task by task, when smartest is the wrong thing to be paying for.&lt;/p&gt;

&lt;p&gt;Companion piece: &lt;a href="https://thekilted.dev/convergent-evidence/" rel="noopener noreferrer"&gt;Convergent evidence&lt;/a&gt;. Nine orchestration-shaped tools crossed the desk while this was being built, and not one became a dependency.&lt;/p&gt;

&lt;p&gt;The policy itself, extracted and de-identified: &lt;a href="https://github.com/thekilteddev/routing-policy" rel="noopener noreferrer"&gt;the routing-policy repo&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://thekilted.dev/route-dont-guess/" rel="noopener noreferrer"&gt;thekilted.dev/route-dont-guess&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>opensource</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>The dashboard accused itself</title>
      <dc:creator>the kilted dev</dc:creator>
      <pubDate>Sun, 02 Aug 2026 20:31:11 +0000</pubDate>
      <link>https://dev.to/thekilteddev/the-dashboard-accused-itself-3adf</link>
      <guid>https://dev.to/thekilteddev/the-dashboard-accused-itself-3adf</guid>
      <description>&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/gyQSfmDN9zw"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;Our own cost dashboard flagged the day it was born as a 26.5x overspend. The transcript said otherwise. Making sure it never happened to anyone else took a bug report, and three days later the fix shipped.&lt;/p&gt;

&lt;p&gt;We build a lot of small internal dashboards. One of them tracks Claude Code token spend across the projects on this machine. It leans on an open-source engine called &lt;a href="https://github.com/getagentseal/codeburn" rel="noopener noreferrer"&gt;CodeBurn&lt;/a&gt;: free, local-only, reads session logs and estimates what each session would have cost on pay-as-you-go pricing. CodeBurn has a panel called optimize, and the panel's job is to flag waste. Sessions that cost far more than a project's own average, with no obvious reason why.&lt;/p&gt;

&lt;p&gt;One day the panel flagged something inside its own house. The dashboard's build session, the day it came into existence, sat there marked as a 26.5x cost outlier. Twenty-six and a half times the project's own average. That is the kind of number designed to make you flinch.&lt;/p&gt;

&lt;p&gt;We didn't flinch. We pulled the raw transcript.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually in there
&lt;/h2&gt;

&lt;p&gt;It ran to 1,070 events. A brainstorm, a plan, twenty-four build agents dispatched, and a final commit that promoted the dashboard to its first real version. That was the founding session, and founding sessions are expensive. The founding session alone ran 1,045,640 tokens, against an average of 62,940 across twelve sibling sessions that followed — a 16.6x gap on tokens, before cost pricing even enters it.&lt;/p&gt;

&lt;p&gt;The day a project goes from nothing to something costs more than any day after it. A house's foundation costs more than repainting one room, and nobody files that under waste. The panel had measured the founding day against the quiet maintenance days that followed and called the ratio waste. That's a category error rather than a measurement problem. The comparison was never apples to apples, and no cost threshold repairs a comparison that was wrong before the arithmetic started.&lt;/p&gt;

&lt;p&gt;So the panel was right to ask the question. It was wrong about the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two easy exits
&lt;/h2&gt;

&lt;p&gt;We could have ignored the flag. We could have silenced it locally and moved on, which would have worked fine, for us. It would also have left the bug sitting there for everyone else running CodeBurn against a young project, which is every project at some point.&lt;/p&gt;

&lt;p&gt;So we read CodeBurn's source instead. The comparison lives in a function called &lt;code&gt;detectSessionOutliers&lt;/code&gt;, in &lt;code&gt;src/optimize.ts&lt;/code&gt;. Reading it turned up the useful part: the codebase already had the mechanism a fix would need. A wee exclusion list called &lt;code&gt;outlierExclusions&lt;/code&gt;, already doing this exact kind of job elsewhere in the panel, sitting right there.&lt;/p&gt;

&lt;p&gt;That gave us something better to file than a complaint. &lt;a href="https://github.com/getagentseal/codeburn/issues/664" rel="noopener noreferrer"&gt;Issue #664&lt;/a&gt;: what happens, why it's wrong, and the specific lines to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The maintainer's fix was better than ours
&lt;/h2&gt;

&lt;p&gt;CodeBurn has an active maintainer, and the pull request was open by the next morning.&lt;/p&gt;

&lt;p&gt;Our sketch excluded every project's founding session, permanently. The maintainer's version was sharper. It excludes the founding session only while a project is young, and it measures young by how many costed sessions the project has behind it rather than by wall-clock age.&lt;/p&gt;

&lt;p&gt;That distinction is the whole fix. Under our version, a genuinely wasteful founding session would have hidden behind the exemption forever. Under the maintainer's, the benefit of the doubt expires after six costed sessions. A founding session in a two-week-old project gets it. The same session in a mature project with months of history does not. Wall-clock age would have missed that, because a project can be old and barely used, and it's the use that makes the average mean anything.&lt;/p&gt;

&lt;p&gt;Three new tests. The 86-test suite green.&lt;/p&gt;

&lt;p&gt;The whole arc took three days. Report on the 14th, the maintainer's pull request open by the 15th, merged on the 16th, and out in &lt;a href="https://github.com/getagentseal/codeburn/releases/tag/v0.9.16" rel="noopener noreferrer"&gt;v0.9.16&lt;/a&gt; on the 17th — where the release notes say, in plain words, that a young project's founding session is no longer flagged as a cost outlier. If you run CodeBurn, update it and the bug is gone.&lt;/p&gt;

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

&lt;p&gt;None of this needed luck. It needed not trusting a scary number at face value, then reading the data underneath it. The rest was writing the bug report the way you would want one written to you: the exact code path, the root cause instead of the symptom, and a fix that was ready to review.&lt;/p&gt;

&lt;p&gt;The tool was wrong, and it was wrong in a way worth the time it took to explain properly. Silencing the flag locally would have fixed our dashboard. It would have left the bug exactly where it was.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://thekilted.dev/dashboard-accused-itself/" rel="noopener noreferrer"&gt;thekilted.dev/dashboard-accused-itself&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>debugging</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>We were cloning a copy</title>
      <dc:creator>the kilted dev</dc:creator>
      <pubDate>Mon, 27 Jul 2026 07:57:44 +0000</pubDate>
      <link>https://dev.to/thekilteddev/we-were-cloning-a-copy-2a2p</link>
      <guid>https://dev.to/thekilteddev/we-were-cloning-a-copy-2a2p</guid>
      <description>&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/wk2fUUoqkmw"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;A product walkthrough needed a narrator: not a person, a synthesised voice reading a script over the screen recording. Simple enough on paper.&lt;/p&gt;

&lt;p&gt;We wanted four things from that voice at once, and it turned out you cannot fully satisfy all four together.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scottish&lt;/strong&gt; — because the product carries a real name behind it, and a bland mid-Atlantic voice reading it back would have been wrong from the first sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural&lt;/strong&gt; — not like a robot reading a list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local&lt;/strong&gt; — free and offline, no per-render cloud bill, no dependency on someone else's server staying up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obtainable&lt;/strong&gt; — a thing we could actually get running on this machine, today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Almost every option we tried hit three of those and quietly broke the fourth. The whole hunt is really about which corner each candidate painted us into, and how the problem eventually stopped having corners at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Piper, and the number that came out wrong
&lt;/h2&gt;

&lt;p&gt;We started with &lt;a href="https://github.com/rhasspy/piper" rel="noopener noreferrer"&gt;Piper&lt;/a&gt;, a small, fast, fully local text-to-speech engine, using a voice called &lt;code&gt;en_GB-alba-medium&lt;/code&gt;. Alba was the one that mattered: a genuine Scottish speaker. Local, free, offline, right accent. Three of four, immediately.&lt;/p&gt;

&lt;p&gt;Two problems. The first was almost funny: Piper read grouped numbers wrong. A figure like 222,000 didn't come out as "two hundred and twenty-two thousand," it came out mangled. We fixed it by rewriting the script instead of the engine: every figure got spelled out as words before it ever reached the synthesiser. That fix turned out to be engine-independent. It survived every voice we swapped in after, which was the first small lesson of the whole hunt.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fix the input, not the tool, and the fix outlives the tool.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The second problem was the real wall. Individual words were fine; whole sentences were robotic. The rise and fall across a sentence, the prosody, was flat, and that isn't a knob you can turn on Piper. Sentence-level prosody is a hard ceiling of that engine. Getting past it meant leaving Piper, and that's where the corners started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five candidates, five different ways to lose
&lt;/h2&gt;

&lt;p&gt;We ran through the alternatives. Every one was rejected, and the interesting part is that no two were rejected for the same reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://huggingface.co/hexgrad/Kokoro-82M" rel="noopener noreferrer"&gt;Kokoro-82M&lt;/a&gt;&lt;/strong&gt;, a different local engine, voice &lt;code&gt;bf_emma&lt;/code&gt;. Clearer than Piper, genuinely more natural, and judged "really bland and unnatural compared to the alba voice." It won on naturalness and lost on character. We kept it wired in behind a switch, since it worked and there was no reason to delete working code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/SWivid/F5-TTS" rel="noopener noreferrer"&gt;F5-TTS&lt;/a&gt;, cloning alba.&lt;/strong&gt; The clever idea: take an engine with far better prosody than Piper and clone the alba voice into it. Best of both, alba's accent riding F5's flow. The result was "brutal, loses the alba and sounds more English," heard instantly. Cloning a synthetic reference, a voice that was itself already generated, flattened the very character we were trying to keep. Getting even that far meant building an isolated Python environment on the CPU, because the machine's GPU stack was too new for the tool.&lt;/p&gt;

&lt;p&gt;Won on flow, lost on accent, cost real setup time. Rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud, &lt;a href="https://elevenlabs.io" rel="noopener noreferrer"&gt;ElevenLabs&lt;/a&gt;.&lt;/strong&gt; The one path to genuinely Scottish and natural at once. The two free Scottish female voices weren't right, and paying for the better tier wasn't on the table. Parked, not killed. Worth noting the trade would have been softer than it sounds: it's cloud at build time only, you synthesise the script once and the video that ships is a local file. But parked is parked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI" rel="noopener noreferrer"&gt;RVC&lt;/a&gt; voice conversion.&lt;/strong&gt; The technically correct local route: convert an expressive voice's output into alba's timbre. Research-grade effort, the same environment friction as everything else, and a real risk of coming out muddy. Declined at this stage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CereProc's "Heather / The Scottish Voice."&lt;/strong&gt; This one stung, because it was the ideal: a real, high-quality Scottish voice that installs as a normal local system voice. Everything we wanted, except we couldn't get it. The shop that sells it was closed, and the free route needs a Scottish public-sector, education, or NHS email address we don't have. Won on every axis except the one that reads "can you actually obtain it." No.&lt;/p&gt;

&lt;p&gt;Five candidates, and between them they covered all four requirements. Just never four at once, never in the same voice.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Voice&lt;/th&gt;
&lt;th&gt;Scottish&lt;/th&gt;
&lt;th&gt;Natural&lt;/th&gt;
&lt;th&gt;Local&lt;/th&gt;
&lt;th&gt;Obtainable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Piper (&lt;code&gt;en_GB-alba-medium&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kokoro-82M (&lt;code&gt;bf_emma&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F5-TTS (cloning alba)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElevenLabs (cloud)&lt;/td&gt;
&lt;td&gt;➖&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RVC (voice conversion)&lt;/td&gt;
&lt;td&gt;➖&lt;/td&gt;
&lt;td&gt;➖&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CereProc ("Heather")&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;✅ met · ❌ broke · ➖ never established (never run far enough to find out)&lt;/p&gt;

&lt;h2&gt;
  
  
  The ceiling we accepted
&lt;/h2&gt;

&lt;p&gt;So we stopped chasing and accepted a ceiling. The voice we shipped first was alba again, but with a phrasing pass over the whole script: long strings broken into short breath-group sentences, figures landing at the ends of sentences where the voice naturally slows, the brand name written the way it should sound. A couple of prosody settings pushed to slow it down and loosen it up. The honest verdict was "a step up from robot." Tolerable. It shipped.&lt;/p&gt;

&lt;p&gt;If the story ended there, it would be a fine wee story about accepting good-enough. It didn't end there, because of one fact we didn't have yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The corpus that was already ours
&lt;/h2&gt;

&lt;p&gt;We found the &lt;a href="https://doi.org/10.7488/ds/2506" rel="noopener noreferrer"&gt;Alba speech corpus&lt;/a&gt; on Edinburgh DataShare. Licensed CC BY 4.0: free to use, fine-tune, even use commercially, as long as you attribute it. And here's what made it matter. It is the actual source corpus behind Piper's alba voice. The speaker we'd been trying to preserve through clone after clone was sitting right there as raw studio recordings: roughly four hours, 4,613 matched audio-and-text pairs, 48kHz, recorded in a hemi-anechoic room, clean.&lt;/p&gt;

&lt;p&gt;That reframes the whole problem. Up to this point the question had been "which voice do we pick." Now it was "can we reproduce this specific speaker inside a better engine." Not clone a synthetic echo of her. Train on her actual voice. A different question, with a different and better answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheapest first, and a failure that explained everything
&lt;/h2&gt;

&lt;p&gt;We staged it cheapest-first: try the free thing before the expensive thing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero-shot&lt;/strong&gt; — hand the expressive engine (XTTS-v2) a real human clip of alba and ask it to imitate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tune&lt;/strong&gt; — if that failed, actually fine-tune the engine on the corpus.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud&lt;/strong&gt; — if that failed too, cloud.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stage 1 failed, as predicted, and the failure was the useful part. Even with a genuine human reference, the voice "wildly drifts off Scottish." Here's why, and it's the structural lesson of the whole hunt: zero-shot cloning copies the timbre but regenerates the prosody from the engine's own base model, and that base model is English-dominant. The accent gets ironed out no matter how good or how long the reference clip is. That single fact retroactively explained the F5 disaster too.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cloning was never going to hold a Scottish accent. Only training could.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On to Stage 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  The environment, wall by wall
&lt;/h2&gt;

&lt;p&gt;Fine-tuning a neural voice model on a Windows machine with a consumer GPU is a series of small walls, each of which stops you dead until you find the one specific thing wrong. In order:&lt;/p&gt;

&lt;p&gt;The machine's antivirus intercepts SSL, which breaks the package installer for any fresh environment. We had to point it at the antivirus's own certificate to install anything at all. The training library demanded a recent version of a dependency, but the newest version of that dependency had deleted a function the library still called, so we pinned it below the break. The obvious PyTorch build wanted an extra media component that's painful to install on Windows; we pinned an earlier, matched build instead, and even that took force because the installer kept deciding the wrong build was already satisfied and skipping it. On Windows, the training loop has to be guarded with the standard &lt;code&gt;if __name__ == "__main__"&lt;/code&gt; incantation, or the parallel data-loading workers each re-run the whole training script and collide on a locked file.&lt;/p&gt;

&lt;p&gt;The GPU is an RTX 5060 with 8GB VRAM, but it's also driving the monitor, so only about 5GB was actually free, which forced the smallest possible batch size. And the natural move to save memory, half-precision training, produced &lt;code&gt;loss = nan&lt;/code&gt; from the very first step: half-precision overflowed inside the model's forward pass, and only full precision at batch-size-one trained cleanly. The reference recipe we were adapting had quietly omitted half-precision for exactly this reason. It always tells you afterwards.&lt;/p&gt;

&lt;p&gt;Every one of those was a full stop until it was solved. None of them was the interesting problem. All of them stood between us and the interesting problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The last mile was in the settings, not the training
&lt;/h2&gt;

&lt;p&gt;Then the real limit: training ran at about 8.7 seconds per step, and a complete run would have taken fifteen to eighteen hours. Not happening. It didn't need to finish, though. The adaptation levels off, and around checkpoint 1,000 the voice had clearly turned: "much better, much clearer." The accent held. That was the whole question, whether training could recover the accent that cloning couldn't, and the answer was yes.&lt;/p&gt;

&lt;p&gt;Two blemishes were left: a wee stutter, and the first word of each line pitched a bit high. Neither needed more training. They were inference settings: synthesise the text sentence-by-sentence instead of all at once, nudge one sampling temperature. Stutter gone. Along the way we had to correct a belief we'd been carrying: that using the desktop while it generated was corrupting the output. It wasn't. The model's output is deterministic; what the GPU is doing during playback has nothing to do with the saved file. We'd been blaming the environment for a settings problem, which, after a whole hunt of genuine environment walls, is an easy mistake to make and a telling one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two loose ends, closed
&lt;/h2&gt;

&lt;p&gt;The voice shipped: a locally fine-tuned Scottish narrator, trained on the real speaker, running offline. But "it works" and "it's finished" are different claims, and two small things after the fact prove it.&lt;/p&gt;

&lt;p&gt;First, acronyms came out garbled, the model tried to pronounce them as words. The fix was to spell them phonetically in the script: an initialism became "aitch em ar see," a web address became "guhv dot you kay." That last one has a detail worth keeping: we spelled it "guhv," not "gov," because "gov" gave the voice a long o and we wanted the short flat vowel a person actually uses. The on-screen text was never touched, only what the voice reads.&lt;/p&gt;

&lt;p&gt;Second, the finished video sounded quiet. Instead of guessing, we measured it: the audio sat at −26.6 LUFS, where spoken content usually lives around −16. Not subjective. The synthesiser's raw output was just quiet, and nothing in the pipeline had ever corrected for it. One normalisation pass, re-rendered, remeasured: −16.2. On target.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still open
&lt;/h2&gt;

&lt;p&gt;The regen script's default engine still points at Piper, not the fine-tuned voice, on purpose: a missing training environment can't hard-fail a regen if the fallback is the simple engine. The good voice ships by setting an explicit flag. Whether to flip that default is a live, unresolved call, not an oversight, and it's still open.&lt;/p&gt;

&lt;p&gt;Strip the specifics and it comes down to this. A want with four constraints that don't all fit at once. A row of honest dead ends, each failing for its own real reason. One piece of information, a corpus with the right licence, that turned "pick a voice" into "reproduce a speaker." A gauntlet of environment walls that are exhausting, boring, and mandatory. And at the end, the last defects weren't in the hard thing, the training. They were in the easy thing, the settings, that we'd stopped questioning.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://thekilted.dev/cloning-a-copy/" rel="noopener noreferrer"&gt;thekilted.dev/cloning-a-copy&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>texttospeech</category>
      <category>ai</category>
      <category>opensource</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
