<?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: InferHaven</title>
    <description>The latest articles on DEV Community by InferHaven (@inferhaven).</description>
    <link>https://dev.to/inferhaven</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%2F3986382%2Fb70b5bca-f72b-45d8-a3f3-9996dd271234.png</url>
      <title>DEV Community: InferHaven</title>
      <link>https://dev.to/inferhaven</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/inferhaven"/>
    <language>en</language>
    <item>
      <title>How I built a tutor that refuses to write your code</title>
      <dc:creator>InferHaven</dc:creator>
      <pubDate>Wed, 29 Jul 2026 18:54:58 +0000</pubDate>
      <link>https://dev.to/inferhaven/how-i-built-a-tutor-that-refuses-to-write-your-code-2pf5</link>
      <guid>https://dev.to/inferhaven/how-i-built-a-tutor-that-refuses-to-write-your-code-2pf5</guid>
      <description>&lt;p&gt;Two weeks ago I launched &lt;a href="https://codetrain.ai" rel="noopener noreferrer"&gt;CodeTrain&lt;/a&gt;, an AI tutor with one rule: it never writes your code. You type every line, it plans the steps, runs what you wrote, and grades it.&lt;/p&gt;

&lt;p&gt;People probably assume the rule is a prompt. Something like "do not write code for the user," pasted at the top of a system message, and done. That was the first version. It lasted about an hour.&lt;/p&gt;

&lt;p&gt;What follows is what the rule actually cost to build: a grading contract instead of a chat reply, two separate model calls that must never be merged, two execution paths so the free tier costs almost nothing to run, and a short list of things that broke in front of real users.&lt;/p&gt;

&lt;h2&gt;
  
  
  A refusal is not prompted, its contracted
&lt;/h2&gt;

&lt;p&gt;Here is the failure mode nobody warns you about. Ask a model to teach rather than solve, and it agrees enthusiastically. Then the learner gets stuck, and the model helps. It writes "you could try something like this" and drops in four lines. Technically it did not actually solve the exercise. Practically the lesson is over..&lt;/p&gt;

&lt;p&gt;The fix to this was to stop asking for prose and start requiring a decision. Every grading turn returns JSON with a fixed shape, or it is treated as a failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"advance"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"retry"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"comment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"feedback_md"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;concise Socratic markdown&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prints exactly Hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"learned"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;short concept the learner demonstrated&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single change did more for the product than any amount of instruction tuning in the prompt. A model writing prose has infinite room to be helpful in the wrong direction. A model that has to pick &lt;code&gt;advance&lt;/code&gt; or &lt;code&gt;retry&lt;/code&gt;, and list the criteria it checked with a boolean next to each, has to commit to a judgment about the learner's code.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agreeable by default, because agreeing reads as helpful&lt;/td&gt;
&lt;td&gt;Has to commit: advance, retry, or comment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slides into writing the fix when the learner struggles&lt;/td&gt;
&lt;td&gt;Every criterion carries a boolean the learner can see&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No stable signal for the UI to render&lt;/td&gt;
&lt;td&gt;Failing criteria are required, so the gap is visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impossible to tell a pass from a polite non-answer&lt;/td&gt;
&lt;td&gt;The UI renders check marks instead of paragraphs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;checks&lt;/code&gt; array is the part learners actually respond to. Each item is one concrete criterion with pass or fail, and the prompt requires failing criteria to be included rather than quietly dropped. You see exactly which two of five things your code did, which is a very different experience from a paragraph explaining that you are on the right track.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the two calls apart
&lt;/h2&gt;

&lt;p&gt;Lesson authoring and grading are separate calls, with separate prompts, and merging them is the single worst thing you can do to a tutor like this.&lt;/p&gt;

&lt;p&gt;I know because I tried it. One call, full context, plan the next step and grade the last one at the same time. It saves a round trip and it is meaningfully cheaper. It also produces a tutor that could write the answer into the question. When the same call that just saw a struggling learner also composes the next step, the step it writes gets suspiciously specific. "Now add the &lt;code&gt;except KeyError&lt;/code&gt; branch that returns an empty list" is just the solution with a task label on it.&lt;/p&gt;

&lt;p&gt;Separated, the authoring call never sees the failure and cannot overfit to it. That constraint is now enforced in the codebase itself: control-plane bookkeeping stays in the router, the tutor engine stays synchronised with the agent's copy of the prompt, and a captured-prompt test fails the build if the two drift apart.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The rule the grader needed most&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "never write code." That part was easy. The hard rule was: do not be pedantic. Early graders would refuse to advance correct code because the learner had not exercised every branch at run time. A correct &lt;code&gt;if/else&lt;/code&gt; is correct even if only one side ran. The prompt now says so explicitly, and instead of gating, it politely suggests an input that would show the other branch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Two execution paths, and why the free tier does none of the work
&lt;/h2&gt;

&lt;p&gt;A tutor that grades code has to run code. The obvious design puts a container behind every Run button, and it is the reason a lot of similar products have no free tier, or one with a queue in front of it.&lt;/p&gt;

&lt;p&gt;Python and JavaScript run entirely in the learner's browser. Python goes through Pyodide in a dedicated worker, JavaScript runs in its own worker, and both stay pooled so the second Run does not pay startup again. A prewarm kicks off when the lesson opens, which usually finishes before anyone types their first line. Runs are capped at 20 seconds, which is generous for a teaching step and short enough that a runaway loop does not hang the tab.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On the free tier, the Python and JavaScript a learner writes never reaches a server, which is the only reason the tier can exist without a card and without a queue.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;free tier economics&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That holds for the two languages most lessons use. bash, ruby, perl and php genuinely need a real interpreter and a filesystem, so those go to an isolated server sandbox with per-minute and per-day caps, on free accounts too. The browser runtimes cover the common case; the sandbox covers what a browser cannot honestly fake.&lt;/p&gt;

&lt;p&gt;The measured cost tracks either way. Across a recent two-week window the managed model spend for every free-tier session on the platform came to about 25 cents total. Compute for running the learner's code was zero, because it happened on their machine, in a tab they already had open.&lt;/p&gt;

&lt;p&gt;The languages that cannot run in a browser fall back to a real sandbox on our infrastructure: bash, ruby, perl and php, with a real shell and real files, no network, and per-user rate and daily caps. That sandbox is Alpine with BusyBox, which produced its own class of bug. The grader would demand a GNU coreutils flag that BusyBox does not implement, then fail a learner whose solution was correct. The prompt now names that constraint directly and instructs the grader to accept any working BusyBox-compatible approach.&lt;/p&gt;

&lt;p&gt;Dockerfile lessons get static linting instead of a real build. Letting anonymous users build containers on my infrastructure is a speedrun to an incident writeup, and the lesson value was in the file, not the image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that broke in front of real users
&lt;/h2&gt;

&lt;p&gt;If you are going to skip ahead to anything in this article, here is the interesting stuff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model emitted JSON that was not JSON.&lt;/strong&gt; Specifically, feedback containing a stray backslash, usually from a regex or a Windows path in the learner's code, which invalidated an otherwise perfect verdict. The parser now repairs unescaped backslashes before parsing, and falls back to a &lt;code&gt;retry&lt;/code&gt; carrying the raw text if it still cannot be read. A tutor that hard-fails because a learner typed &lt;code&gt;\d&lt;/code&gt; is not a tutor that can function well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Older model output used plain strings for checks&lt;/strong&gt; instead of objects with a label and a boolean. Rather than break those, the coercion layer treats a bare string as a passing check. It is not elegant. It is the difference between rendering something useful and rendering an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tutor asked permission to continue.&lt;/strong&gt; Early versions ended a correct submission with "ready to move on?" Every single time, learners answered the question instead of coding, and the lesson turned into a conversation. Advancing is now the confirmation, stated as a hard rule in the prompt: when the submission is correct, advance, do not ask.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser Python is not Python.&lt;/strong&gt; No subprocess, no real files, no network. A learner following an authored step involving &lt;code&gt;subprocess&lt;/code&gt; was hitting a wall the tutor had built for them. The grader is now told to never fault the learner for a runtime limit, because that step should not have been written in the first place, and to guide toward a runnable approach or simply advance. I will be improving this behaviour into the future as well.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The pattern in all four&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every one of these is a case where the model needed to be guided to perform as a more acceptable tutor instead of just a sophisticated chatbot. Each one had a simple yet effective fix and I plan to be always improving CodeTrains effectiveness as a tutor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Repo mode and the patch at the end
&lt;/h2&gt;

&lt;p&gt;The free tier teaches on examples and any public repository you point it at. Paid repo mode runs against a real checkout on your machine, which raises a question I got wrong at first: what happens to the code you wrote?&lt;/p&gt;

&lt;p&gt;The first design applied each step as its own commit. It felt responsive and it was a mistake. Nobody wants seven commits titled "step 3" in their history, and a half-finished lesson left the working tree in a state the learner had to clean up.&lt;/p&gt;

&lt;p&gt;Now the lesson holds everything and proposes one patch at the end, containing only the code you typed, against a branch you choose. You review it like any other diff. If you abandon the lesson, nothing touches your repository at all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One rule that has not moved&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The repo reader refuses secrets. The denylist that keeps &lt;code&gt;.env&lt;/code&gt; files and key material out of lesson context is mirrored in two places, the local agent and the control plane, and changing one without the other is treated as a bug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What I'm tuning next
&lt;/h2&gt;

&lt;p&gt;The tutor is at its best on a focused concept in code it can read directly. Lesson scope is the dial still getting tuned in: a tight lesson on one module lands harder than one that ranges across a whole framework, so more of the authoring work is going into keeping lessons narrow by default.&lt;/p&gt;

&lt;p&gt;Browser Python runs on Pyodide, whose package set is a subset of PyPI. The prompt-level guardrails already steer authored steps toward what the runtime can actually import, and widening that coverage, including routing more lesson types to the server sandbox where it makes sense, is the next piece of that work.&lt;/p&gt;

&lt;p&gt;The grading loop is a model making a judgment, so the design choice was to make the judgment inspectable. Every criterion is listed with a pass or fail rather than one overall score, which means you can always see the basis for a verdict instead of blind faith in the model itself. That visibility is also our own best feedback channel: every reported step comes back with the checks attached, which is how the tuning above gets prioritised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try to break it
&lt;/h2&gt;

&lt;p&gt;The free tier runs in your browser, ten lessons a month, no card and no trial clock: &lt;a href="https://codetrain.ai" rel="noopener noreferrer"&gt;codetrain.ai&lt;/a&gt;. Point it at a public repository you did not write and ask for a lesson on the part you understand least. That is the fastest honest test of everything above.&lt;/p&gt;

&lt;p&gt;If you talk it into writing your code outright, send me the screenshot. I will fix the loop, and probably frame the screenshot!&lt;/p&gt;

&lt;p&gt;-- Ethan L.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://inferhaven.com/blog/2026-07-29-tutor-that-refuses-to-write-code" rel="noopener noreferrer"&gt;InferHaven blog&lt;/a&gt;. I'm the founder, and I'm in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>codetrain</category>
      <category>ai</category>
      <category>python</category>
      <category>sandboxing</category>
    </item>
    <item>
      <title>I built a production AI agent for our NOC ticket queue in one shift</title>
      <dc:creator>InferHaven</dc:creator>
      <pubDate>Fri, 17 Jul 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/inferhaven/i-built-a-production-ai-agent-for-our-noc-ticket-queue-in-one-shift-3fka</link>
      <guid>https://dev.to/inferhaven/i-built-a-production-ai-agent-for-our-noc-ticket-queue-in-one-shift-3fka</guid>
      <description>&lt;p&gt;Our NOC gets a steady stream of tickets every day, and a good chunk of them are routine: the same site issue reported twice, a ticket that just needs the right category to be organized clearly, a slow-connection report that needs proper network diagnostics. None of that requires real judgment. All of it eats time.&lt;/p&gt;

&lt;p&gt;I'd already been using OpenCode to just mess around and test things for free while on shift, so I decided to wire up an autonomous agent to reduce some of the repetitive workload of managing our tickets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it actually does&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each separate task of the workflow basically lives in a skill file, loaded by the OpenCode agent in correct order with an AGENTS.md to orchestrate the entire workflow, pretty standard.&lt;br&gt;
A webhook receives a new ticket, waits sixty seconds to see if more come in (batching saves a lot of redundant work), then hands the batch to OpenCode running in non-interactive mode. From there it checks for duplicates against other open tickets for the same site and issue, categorizes the ticket by type, and for anything flagged as a slow connection, kicks off a deeper network diagnostic and posts the result straight to the ticket. It pings the team in chat only for tickets that actually need a human, which is still most of them now as its main job here to start is simply take on 80% of the initial ticket analysis and investigation workload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually took the day&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The AI part was the easy part, weirdly. What ate the hours was the plumbing underneath it. Duplicate detection needed exact-match site names, and site naming in our system turned out messier than expected, learned that one the hard way. The network diagnostic tool uses different naming conventions than the ticket system for the same physical sites, so there's a whole lookup step just to translate between the two. The first version had a five-minute timeout on the diagnostic step; turns out some sites take seven-plus minutes to fully analyze especially if there is an issue causing network slowdowns on the way. I bumped the default timeout to ten and made configurable via the .env file so I'm not guessing next time. And there's a fallback notification for when the agent itself crashes or runs out of budget, because a ticket disappearing silently is worse than the ticket never being automated at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it landed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's running as a systemd service now, processing real tickets. Duplicates get caught and routed without anyone touching them. The diagnostic runs automatically on the tickets that need it. The team only sees what actually needs attention, and the total ticket count is already trending down since it went live.&lt;/p&gt;

&lt;p&gt;The part that surprised me most wasn't that an AI agent could do this. It's that the whole thing, idea to running in production, fit inside one shift alongside the other daily workloads I always have going. A year ago this would've been a sprint's worth of work for a team, and half of it would've gone to exactly the plumbing described above, not the interesting part.&lt;/p&gt;

&lt;p&gt;If you're running a similar agent, curious what your batching and timeout setup looks like. Feels like the kind of dead simple thing everyone's probably independently reinventing right now.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>networking</category>
      <category>automation</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>InferHaven</dc:creator>
      <pubDate>Thu, 16 Jul 2026 12:49:51 +0000</pubDate>
      <link>https://dev.to/inferhaven/-3j7f</link>
      <guid>https://dev.to/inferhaven/-3j7f</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/inferhaven/i-built-an-ai-tutor-that-refuses-to-write-your-code-5d01" class="crayons-story__hidden-navigation-link"&gt;I built an AI tutor that refuses to write your code&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/inferhaven" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3986382%2Fb70b5bca-f72b-45d8-a3f3-9996dd271234.png" alt="inferhaven profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/inferhaven" class="crayons-story__secondary fw-medium m:hidden"&gt;
              InferHaven
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                InferHaven
                
              
              &lt;div id="story-author-preview-content-4156227" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/inferhaven" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3986382%2Fb70b5bca-f72b-45d8-a3f3-9996dd271234.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;InferHaven&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/inferhaven/i-built-an-ai-tutor-that-refuses-to-write-your-code-5d01" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 16&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/inferhaven/i-built-an-ai-tutor-that-refuses-to-write-your-code-5d01" id="article-link-4156227"&gt;
          I built an AI tutor that refuses to write your code
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/learning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;learning&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/coding"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;coding&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/inferhaven/i-built-an-ai-tutor-that-refuses-to-write-your-code-5d01" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/inferhaven/i-built-an-ai-tutor-that-refuses-to-write-your-code-5d01#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>I built an AI tutor that refuses to write your code</title>
      <dc:creator>InferHaven</dc:creator>
      <pubDate>Thu, 16 Jul 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/inferhaven/i-built-an-ai-tutor-that-refuses-to-write-your-code-5d01</link>
      <guid>https://dev.to/inferhaven/i-built-an-ai-tutor-that-refuses-to-write-your-code-5d01</guid>
      <description>&lt;p&gt;Somewhere on your team right now, a pull request is getting approved by someone who couldn't rewrite it from scratch. Not because they're lazy. The assistant wrote it, the tests passed, the diff looked plausible, and the sprint doesn't stop for philosophy. Do that for a year and you get a team that ships faster every quarter and understands a little less of its own codebase every quarter too.&lt;/p&gt;

&lt;p&gt;I ran into this a few times while building the start of InferHaven. The whole point of this company is that you shouldn't have to hand your code to someone else to get the benefits of AI. But there's a second thing quietly leaving the building, and no firewall catches it: the skill. When the model types and the human tabs through, the knowledge of how your system actually works stops living in your engineers and starts living in a vendor's autocomplete.&lt;/p&gt;

&lt;p&gt;So I built the opposite tool. Today I'm launching &lt;strong&gt;CodeTrain&lt;/strong&gt;: a hands-on AI trainer that teaches developers on their &lt;em&gt;own&lt;/em&gt; codebase, and refuses to write the code for them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;**The learner writes every line. CodeTrain plans the lesson, sets up each step, runs your code, and grades the result, but it will not type your solution. Not even a "tiny" one-liner. When you're stuck it shrinks the step or gives you a sharper hint. It does not give you the answer, because the answer was never the point.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What a lesson actually looks like
&lt;/h2&gt;

&lt;p&gt;You ask CodeTrain something real. "Walk me through adding a health check to this Dockerfile." "I don't actually understand our retry logic, teach it to me." It breaks that into a short lesson, usually two to six steps, each one small enough to hold in your head.&lt;/p&gt;

&lt;p&gt;Each step is one concrete thing to write in a syntax-highlighted editor with a Run button. You type it. The code executes immediately: Python and JavaScript run right in your browser, shell and a few other runtimes run in an isolated sandbox, web stuff renders in a live preview. Then the tutor reviews what you wrote against the step's criteria, shows you ✓ and ✗ per criterion, and either advances you or asks the question that makes you see what's off. Fail a step multiple times and it doesn't dump the solution on you. It cuts the step in half.&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="nv"&gt;$ &lt;/span&gt;codetrain start &lt;span class="nt"&gt;--mode&lt;/span&gt; repo
Session a6502084e8254276a394d5be578e9229 started &lt;span class="o"&gt;(&lt;/span&gt;repo mode&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
  Plan:     team
  Modes:    sandbox, repo
  Sessions: 0/1000 this month
  Usage:    0% of your monthly allowance &lt;span class="o"&gt;(&lt;/span&gt;100% left&lt;span class="o"&gt;)&lt;/span&gt;
  Models:   haiku, sonnet

Your tutor is open &lt;span class="k"&gt;in &lt;/span&gt;the browser:
    http://127.0.0.1:7341
&lt;span class="o"&gt;(&lt;/span&gt;workspace: /tmp/codetrain-repo-v555lhi7/repo&lt;span class="o"&gt;)&lt;/span&gt;

Tutoring is live &lt;span class="k"&gt;in &lt;/span&gt;your browser — keep this window open. Ctrl-C to stop.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz48kolf4favo9soj30t8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz48kolf4favo9soj30t8.png" alt="Starting repo mode" width="662" height="723"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Before the lesson: pick your level, how much help you want, and whether the session may build toward a real edit. The goal here is the health check from the terminal above.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's repo mode, the paid tier. The tutor reads your actual repository, builds lessons out of your actual code, and at the end of a guided lesson (can run exploration only if desired) it proposes one integrated patch that lands the code &lt;em&gt;you&lt;/em&gt; wrote onto your real branch. You review it like any other diff. Free tier does not include repo mode and drops you into sandbox practice instead: same tutor, same you-type-it rule, generated exercises from natural language.&lt;/p&gt;

&lt;p&gt;Here is that health-check lesson, start to finish. Step 2 has you draft the probe on a scratch Dockerfile where mistakes are free:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmantz6inh971t0cdwudk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmantz6inh971t0cdwudk.png" alt="Test in scratch file" width="800" height="434"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;You write the HEALTHCHECK on scratch first. Run gives you the linter, Send gives you the tutor's verdict against the step's criteria.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Step 3 moves the line you wrote into the real Dockerfile, all 360-odd lines of it, in the one spot where Docker will actually honour it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fweq97hh1yj6j36kcwam5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fweq97hh1yj6j36kcwam5.png" alt="Edits to real file" width="800" height="478"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Same instruction, real file. Placement is the lesson here: Docker only honours the last HEALTHCHECK, so it goes right before ENTRYPOINT/CMD.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Pass the final step and the tutor offers the one targeted patch it guided you on: the diff of what &lt;em&gt;you&lt;/em&gt; typed, applied to your working tree only after you approve it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ekq9dpf29utir12wp9h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3ekq9dpf29utir12wp9h.png" alt="Patchback review" width="670" height="950"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every criterion checked, then the patch-back: your change as a reviewable diff. A .codetrain.bak backup is saved before anything is written.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcujsgdssxpb1ytfhyouv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcujsgdssxpb1ytfhyouv.png" alt="Change shipped" width="799" height="346"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Change shipped. A real HEALTHCHECK lives in the repo, and the recap lists what you practiced, not what was generated for you.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  This is not another assistant
&lt;/h2&gt;

&lt;p&gt;Don't get me wrong. I use coding assistants daily and they're great at their job. Their job is producing code. CodeTrain's job is producing engineers who understand code, and those jobs basically pull in opposite directions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AI assistant (Cursor, Copilot, Claude Code)&lt;/th&gt;
&lt;th&gt;CodeTrain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You describe, it writes&lt;/td&gt;
&lt;td&gt;It plans tiny steps, you write them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output: working code, fast&lt;/td&gt;
&lt;td&gt;Output: a developer who gets it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Understanding is optional&lt;/td&gt;
&lt;td&gt;Understanding is graded, per step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Great when shipping is the goal&lt;/td&gt;
&lt;td&gt;Great when the goal is your team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use both. Seriously. Let the assistant ship the sprint while CodeTrain makes sure the person accepting its diffs could have written them.&lt;/p&gt;

&lt;p&gt;"Code got cheap. Understanding didn't. Train the part that's still expensive."&lt;/p&gt;

&lt;h2&gt;
  
  
  An InferHaven product
&lt;/h2&gt;

&lt;p&gt;InferHaven exists because your code is yours and should stay on your hardware. CodeTrain extends the same instinct to where your code lives secondly: wetware, brains, good ol' humans. Keep the code in-house, keep the skill in-house with it.&lt;/p&gt;

&lt;p&gt;It's built the way you'd expect from us. The agent runs on your machine. Sandbox lessons execute in your browser, not on a stranger's box. Repo mode works against your local checkout. And if you'd rather run lessons through your own model account, bring your own key: Anthropic, OpenRouter, Bedrock, Vertex, or even an Ollama endpoint you host yourself!&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, honestly
&lt;/h2&gt;

&lt;p&gt;The free tier is real: the in-browser trainer, ten sessions a month, streaks and progress tracking, no credit card. It's capped because every managed session costs us actual inference money.&lt;/p&gt;

&lt;p&gt;Pro is $24/month and adds repo mode on your real projects, fair-use session limits, sync across devices, and optional BYOK. Team is $32 per seat and adds the part managers keep asking me about: a dashboard showing who's ramping, per-seat budgets, central billing, and onboarding journeys for new hires. Enterprise is the usual conversation about SSO and code that can't leave your network. If that's you, &lt;a href="mailto:sales@codetrain.ai"&gt;sales@codetrain.ai&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go type something
&lt;/h2&gt;

&lt;p&gt;CodeTrain is live at &lt;a href="https://codetrain.ai" rel="noopener noreferrer"&gt;codetrain.ai&lt;/a&gt;. Sign in, ask it to teach you something you've been faking your way around, and see how it feels to be the one controlling the keyboard again. There's a one-minute demo on the landing page if you want to watch before you commit to the free tier's grand total of... zero dollars.&lt;/p&gt;

&lt;p&gt;InferHaven keeps your code yours. CodeTrain keeps the skill yours. Same haven, different cargo. The beacon is active!&lt;/p&gt;

&lt;p&gt;— Ethan L.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is cross-posted from the &lt;a href="https://inferhaven.com/blog/2026-07-13-introducing-codetrain" rel="noopener noreferrer"&gt;InferHaven blog&lt;/a&gt;. I'm the founder and I'll be in the comments; if you try a lesson and it teaches you something wrong or hands you the answer on a silver platter, I want to hear about it more than I want the compliments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>learning</category>
      <category>coding</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building haven bench in the open, and the flaky CI ghost it flushed out</title>
      <dc:creator>InferHaven</dc:creator>
      <pubDate>Wed, 24 Jun 2026 17:19:43 +0000</pubDate>
      <link>https://dev.to/inferhaven/building-haven-bench-in-the-open-and-the-flaky-ci-ghost-it-flushed-out-1426</link>
      <guid>https://dev.to/inferhaven/building-haven-bench-in-the-open-and-the-flaky-ci-ghost-it-flushed-out-1426</guid>
      <description>&lt;p&gt;&lt;em&gt;A debugging story from building InferHaven in the open: how a benchmark feature flushed out a flaky-CI race that had nothing to do with it.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I shipped a tokens/sec benchmark for local models. The unit tests were green, and then CI turned red in a way that looked like my fault. It wasn't. Here's the whole hunt: a chown that raced the tide, set -e, and a zsh lock file that vanished mid-sweep.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I built a small thing this week and it caught a bigger thing. That's the whole post, really. But the shape of how it happened is worth writing down, because it's the kind of story that usually gets quietly squashed into a one-line commit message and never told. Building in the open means showing the part where the harbor light flickers, not just the part where the boat docks clean.&lt;/p&gt;

&lt;p&gt;The small thing is &lt;code&gt;haven bench&lt;/code&gt;, a command that tells you how fast a model actually runs on your hardware. The bigger thing was a flaky CI failure that wore my new feature as a disguise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small thing: a number you can trust
&lt;/h2&gt;

&lt;p&gt;If you run local models, you live and die by tokens per second. It's the single number everyone in &lt;code&gt;r/LocalLLaMA&lt;/code&gt; trades like baseball cards, and yet most people read it off a vibe: "feels fast on my 3090." I wanted InferHaven to just tell you, honestly, on your own box.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;haven bench qwen2.5-coder:3b &lt;span class="nt"&gt;--runs&lt;/span&gt; 3

  InferHaven bench — qwen2.5-coder:3b-instruct-q4_K_M
  run 1          106.0 tok/s
  run 2          106.3 tok/s
  run 3          106.0 tok/s
  generation     106.1 tok/s  &amp;lt;- decode rate &lt;span class="o"&gt;(&lt;/span&gt;avg of 3 runs&lt;span class="o"&gt;)&lt;/span&gt;
  prompt &lt;span class="nb"&gt;eval   &lt;/span&gt;3507.8 tok/s  &lt;span class="o"&gt;(&lt;/span&gt;40 prompt tokens&lt;span class="o"&gt;)&lt;/span&gt;
  load            0.17 s     &lt;span class="o"&gt;(&lt;/span&gt;run 1: weights -&amp;gt; VRAM&lt;span class="o"&gt;)&lt;/span&gt;
  total           1.39 s     &lt;span class="o"&gt;(&lt;/span&gt;run 1&lt;span class="o"&gt;)&lt;/span&gt;
  method       &lt;span class="nv"&gt;num_predict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;128, &lt;span class="nv"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0, &lt;span class="nv"&gt;temp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0, &lt;span class="nv"&gt;runs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;The headline is &lt;code&gt;generation&lt;/code&gt;, the decode rate. Under the hood Ollama hands back its timings in nanoseconds, and the math is just &lt;code&gt;eval_count / (eval_duration / 1e9)&lt;/code&gt;. The reason that's the honest number and not, say, &lt;code&gt;total&lt;/code&gt; is subtle and important: &lt;code&gt;eval_duration&lt;/code&gt; &lt;em&gt;excludes&lt;/em&gt; model load and prompt ingestion. So it's the pure speed of the model writing tokens, and it holds steady whether the model was cold or already warm in VRAM.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
The first run pays to haul the weights into VRAM (that &lt;code&gt;load&lt;/code&gt; line). The second run doesn't. If you quote "total time" you're really benchmarking your disk and your luck. Decode rate is the speed of the engine itself, the thing that's actually true about the model on your card. That's the figure worth screenshotting.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;One more bit of honesty baked into the output: &lt;code&gt;prompt eval&lt;/code&gt; is gloriously noisy on a short prompt (3507 tok/s above, but it bounces between runs), because you're dividing a tiny token count by a tinier duration. So &lt;code&gt;bench&lt;/code&gt; reports it, but quietly. The number it puts in green, the one it wants you to believe, is generation. A benchmark that oversells itself is just a vibe with extra steps.&lt;/p&gt;

&lt;p&gt;I wrote it the slow way on purpose: the core tokens/sec math by hand, with a unit test seeded from a real run off my RTX 3060, so I could actually explain every line of it instead of cargo-culting a one-liner. Seven assertions, all green. Shellcheck clean. Ran it live against three models. Pushed the PR.&lt;/p&gt;

&lt;p&gt;And then CI turned red.&lt;/p&gt;

&lt;h2&gt;
  
  
  The red light
&lt;/h2&gt;

&lt;p&gt;Two smoke jobs run on every PR: a slim &lt;strong&gt;codespaces&lt;/strong&gt; stack and the full &lt;strong&gt;full-stack&lt;/strong&gt; one. Full-stack went green. Codespaces failed, and not in my test. It failed &lt;em&gt;bringing the container up&lt;/em&gt;, before my code ever ran:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Running the postCreateCommand from devcontainer.json...
Error response from daemon: container 46a4… is not running
postCreateCommand from devcontainer.json failed with &lt;span class="nb"&gt;exit &lt;/span&gt;code 1.
&lt;span class="c"&gt;##[error]Process completed with exit code 1.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;This is the moment that decides whether you're a good engineer or a fast one. The tempting move is: &lt;em&gt;it's my PR, it's the only thing that changed&lt;/em&gt; Poke at the test, re-run it, add a &lt;code&gt;sleep&lt;/code&gt;, wrap something in a &lt;code&gt;|| true&lt;/code&gt; and move on. But if you don't get to the root of the issue it will most likely just surface again.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
No fix without a root cause first. A red test you "fixed" by re-running is just a bug you've agreed to meet again later, usually in front of a stranger.&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;So I did the boring thing instead and actually looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Following the evidence, not the vibe
&lt;/h2&gt;

&lt;p&gt;The accusation was "your PR broke the build." The evidence disagreed, layer by layer.&lt;/p&gt;

&lt;p&gt;What it looked like -- My PR broke CI&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-Only my branch changed
-Red appeared right after I pushed
-It's the new feature, obviously
-Just re-run it / patch the test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;vs&lt;/p&gt;

&lt;p&gt;What the evidence said -- My PR was a bystander&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-My diff touched zero boot-path files
-Full-stack booted the SAME image fine
-The failure was in container startup, before my code ran
-main had failed this exact way before, intermittently
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Three facts did the work. First, my diff touched a CLI command, a library function, a test, and some docs. Nothing in the container's startup path. Second, the &lt;em&gt;full-stack&lt;/em&gt; job built the very same workspace image and came up clean; if my scripts could kill a boot, both jobs would die, not one. Third, the failure happened during &lt;code&gt;up&lt;/code&gt;, before the step that runs my new code ever executed.&lt;/p&gt;

&lt;p&gt;That's not a guilty feature. That's a flaky boot that happened to be standing next to me when the cops showed up.&lt;/p&gt;

&lt;p&gt;The good news is I'd wired the CI to dump the dying container's logs on failure, and the container's last words were the whole case:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;workspace-1 | &lt;span class="nb"&gt;chown&lt;/span&gt;: cannot access &lt;span class="s1"&gt;'/home/haven/.zcompdump-46a4482c870c-5.9.lock'&lt;/span&gt;:
              No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;There it is. &lt;code&gt;46a4482c870c&lt;/code&gt; is the container's own ID. &lt;code&gt;.zcompdump-…-5.9.lock&lt;/code&gt; is a zsh completion lock file, created and deleted in milliseconds while the shell builds its completion cache. And the thing that tripped over it was my entrypoint's first-boot ownership sweep: a recursive &lt;code&gt;chown -R&lt;/code&gt; over the home directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug: a chown that raced the tide
&lt;/h2&gt;

&lt;p&gt;Here's the entire bug, and it's a beauty because there's almost nothing to it:&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;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HAVEN_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HAVEN_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;code&gt;chown -R&lt;/code&gt; walks the tree, builds a list of things to change, then changes them. If a transient file (say, a zsh completion lock) exists when the walk lists it but is &lt;em&gt;gone&lt;/em&gt; by the time &lt;code&gt;chown&lt;/code&gt; reaches it, &lt;code&gt;chown&lt;/code&gt; exits non-zero. And &lt;code&gt;set -e&lt;/code&gt; says: any command that fails, abort the script. So the entrypoint dies. So the container exits. So forty seconds later, when the devcontainer tries to run its setup step, the daemon shrugs and says &lt;em&gt;that container is not running.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's a time-of-check versus time-of-use race, and like all races it only loses sometimes, which is exactly why &lt;code&gt;main&lt;/code&gt; was usually green and only occasionally, mysteriously, wasn't. My PR didn't cause it. My PR just rolled the dice enough times to hit it.&lt;/p&gt;

&lt;p&gt;The fix is the same guard the &lt;em&gt;other&lt;/em&gt; recursive chowns in that file already had. I'd just missed these two:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- chown -R "${HAVEN_USER}:${HAVEN_USER}" "${HOME_DIR}"
&lt;/span&gt;&lt;span class="gi"&gt;+ chown -R "${HAVEN_USER}:${HAVEN_USER}" "${HOME_DIR}" 2&amp;gt;/dev/null || true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;code&gt;|| true&lt;/code&gt; tells &lt;code&gt;set -e&lt;/code&gt; to let this one slide, and a vanished lock file stops being a death sentence for the whole container. I proved the mechanism in isolation first (a deliberately failing &lt;code&gt;chown -R&lt;/code&gt; under &lt;code&gt;set -e&lt;/code&gt; halts the script; the guarded version sails right past it), then shipped it as its own small PR, separate from the benchmark, so the history reads honestly: here's a feature, and here's an unrelated bug the feature flushed out.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;
The instinct that &lt;code&gt;|| true&lt;/code&gt; is "hiding errors" is usually correct. But here the error &lt;em&gt;is&lt;/em&gt; the bug. A recursive chown racing a temp file is a known, benign class of failure, and the surrounding chowns in this same script already tolerate it. The band-aid would've been re-running CI until it was green. Tolerating a known race at the exact line it occurs is the actual fix.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually want you to take from this
&lt;/h2&gt;

&lt;p&gt;The benchmark is nice. Go run &lt;code&gt;haven bench&lt;/code&gt; on your own card and post the number. Honest decode rates are good for everyone, and the more of them in the wild the less anyone has to guess.&lt;/p&gt;

&lt;p&gt;But the part I think is worth more than the feature is the shape of the hunt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A green unit test is not a green build.&lt;/strong&gt; My math was perfect. The bug was three layers away from my math, in startup code I didn't touch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make your failures talk.&lt;/strong&gt; That "dump logs on failure" step cost me five minutes to write months ago and handed me the entire diagnosis in one line. Future-you is a stranger; leave them evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resist the re-run.&lt;/strong&gt; The single most expensive habit in software is treating a flaky test as noise. Flaky almost always means &lt;em&gt;real bug, intermittent trigger.&lt;/em&gt; This one had been quietly failing for weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the root, label it honestly.&lt;/strong&gt; The race got its own PR with its own explanation. Nobody reading the history six months from now has to wonder why a chown grew a &lt;code&gt;|| true&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole reason I'm building this in the open. Not because the wins make good screenshots, but because the &lt;em&gt;misses&lt;/em&gt; are where the actual craft lives, and most of the industry hides them.&lt;/p&gt;

&lt;p&gt;Both PRs are merged. The light's back to steady.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git clone https://github.com/InferHaven/inferhaven-core
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;inferhaven-core
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="nv"&gt;$ &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;ssh haven@localhost
&lt;span class="nv"&gt;$ &lt;/span&gt;haven bench    &lt;span class="c"&gt;# tell me what your card does&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;Float your boat up to the dock, &lt;a href="https://github.com/InferHaven/inferhaven-core" rel="noopener noreferrer"&gt;clone the repo&lt;/a&gt;, run the benchmark, and if you want the managed version when it's ready, the waitlist on the homepage is the way in. The lighthouse is on. And now it doesn't flicker.&lt;/p&gt;

&lt;p&gt;— Ethan L.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>testing</category>
      <category>bash</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
