<?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>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>
