<?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: Blake Yang</title>
    <description>The latest articles on DEV Community by Blake Yang (@datars_7274).</description>
    <link>https://dev.to/datars_7274</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%2F4061230%2F081cb59c-26d8-46fd-a592-ce04b6639b4b.png</url>
      <title>DEV Community: Blake Yang</title>
      <link>https://dev.to/datars_7274</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/datars_7274"/>
    <language>en</language>
    <item>
      <title>The Retry Loop That Ate a Free Tier: A Rate-Limit Postmortem</title>
      <dc:creator>Blake Yang</dc:creator>
      <pubDate>Tue, 25 Aug 2026 08:30:10 +0000</pubDate>
      <link>https://dev.to/datars_7274/the-retry-loop-that-ate-a-free-tier-a-rate-limit-postmortem-m0</link>
      <guid>https://dev.to/datars_7274/the-retry-loop-that-ate-a-free-tier-a-rate-limit-postmortem-m0</guid>
      <description>&lt;p&gt;The webhook handler passed every local test, deployed cleanly, and then took down the free server at 2:14 AM. The symptom looked like a provider quota problem, but the root cause was a single AI-generated loop that retried too eagerly. This post walks through the debugging path from the first 429 to the actual fix, because the same failure pattern is hiding in a lot of generated code.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom: 429s That Appeared Out of Nowhere
&lt;/h2&gt;

&lt;p&gt;The setup was deliberately small: a webhook endpoint generated with MonkeyCode's free model access, hosted on its free server, and connected to a third-party API that occasionally returned 503. MonkeyCode is an open-source project, and its free tier includes a 10-million-token allowance plus a free server instance, which is plenty for a small integration but tight enough to expose amplification bugs. The handler worked for two days, and then legitimate requests started failing with &lt;code&gt;429 Too Many Requests&lt;/code&gt; even though the app was nowhere near its monthly token budget.&lt;/p&gt;

&lt;p&gt;The first debugging instinct was to blame the third-party provider, since 429 is a rate-limit status code and that provider had been flaky all week. A quick check of the provider's status page showed no incident, which shifted the suspicion back to the free server. The server's own logs showed nothing unusual, because the requests that failed were outgoing retries, not incoming traffic.&lt;/p&gt;

&lt;p&gt;The turning point came when the developer replayed the exact failing request sequence locally against the same API and got 200 responses. The code was identical, the payload was identical, and the only difference was the server environment. That local success strongly suggested an environmental cause, so the investigation moved to the server's outgoing traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Investigation: Following the Retry Trail
&lt;/h2&gt;

&lt;p&gt;The access logs on the free server revealed the pattern: every time the downstream API returned a 503, the handler fired off a new request immediately, then another, then another, with no delay between attempts. A single downstream hiccup of three seconds produced forty outgoing requests in under a minute, and the third-party API responded by rate-limiting the server's IP. The logs made the amplification visible in one command:&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;grep&lt;/span&gt; &lt;span class="s2"&gt;"POST /api"&lt;/span&gt; access.log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $4}'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;: &lt;span class="nt"&gt;-f2&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI-generated retry loop looked harmless at first glance because it was short and readable. Here is the simplified version that caused the incident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# keep retrying until the API accepts the request
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop retries forever, it retries instantly, and it treats every 5xx as worth another attempt. On a free tier with a per-minute request cap, that behavior converts a transient downstream error into a self-inflicted outage, because the retries themselves consume the quota that legitimate requests need. The second problem was that the loop ignored the &lt;code&gt;Retry-After&lt;/code&gt; header that the API included in its 429 responses; the API was explicitly saying "wait 30 seconds," and the handler was responding with "how about right now."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause: The Happy Path Is Generated, the Failure Path Is Not
&lt;/h2&gt;

&lt;p&gt;The real lesson is not that the AI model wrote a bad loop, because plenty of human-written loops have the same flaw. The lesson is that generated code tends to be optimized for the happy path, and the failure path is where the assumptions live: retries need backoff, backoff needs jitter, and jitter needs a maximum ceiling. This pattern matters more now that AI coding tools produce more of the codebase, because the happy path is exactly what models generate confidently and the failure path is exactly what they gloss over.&lt;/p&gt;

&lt;p&gt;The fix for this incident had three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Respect the &lt;code&gt;Retry-After&lt;/code&gt; header whenever the downstream service provides one.&lt;/li&gt;
&lt;li&gt;Use exponential backoff with jitter so retries spread out instead of clustering.&lt;/li&gt;
&lt;li&gt;Cap the total retry budget so a stuck service cannot consume the entire free tier.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;MAX_RETRIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;BASE_DELAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MAX_RETRIES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE_DELAY&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;downstream API still failing after retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decision table that now lives next to this code is simple: retry on 429 with the header's delay, retry on 5xx with exponential backoff, and never retry on 4xx validation errors because the request itself is the problem. That last rule matters more than it looks, because retrying a bad payload only multiplies the damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reusable Debugging Workflow
&lt;/h2&gt;

&lt;p&gt;The debugging path that found this bug is worth keeping as a checklist for any AI-generated code that talks to external services. Start with the symptom and verify whether it reproduces outside the failing environment, because a local success strongly suggests an environmental cause. Then trace the outgoing traffic rather than the incoming traffic, since the failing requests were the ones the server initiated, not the ones it received.&lt;/p&gt;

&lt;p&gt;The next step is to look for amplification loops, which are any code path where one external failure produces multiple internal attempts. A quick grep for &lt;code&gt;while True&lt;/code&gt; or &lt;code&gt;retry&lt;/code&gt; in the generated code usually reveals the loop, and counting the outgoing requests per minute confirms whether the loop is amplifying the problem. The final step is to apply the retry decision table and re-run the exact failure scenario, which in this case meant simulating a downstream 503 and watching the outgoing request rate drop from forty per minute to five.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Who Should Skip This Approach
&lt;/h2&gt;

&lt;p&gt;This retry strategy is not a universal fix, and there are setups where it is the wrong tool entirely. If the downstream service requires exactly-once delivery, retries alone are insufficient because a retry can duplicate a side effect, so the handler needs idempotency keys or a deduplication layer on top of the backoff logic. The approach also assumes the caller controls the retry budget, which is not true when the client is a third party that retries aggressively on its own; in that case the server needs rate limiting on the receiving side, not just polite retries on the sending side.&lt;/p&gt;

&lt;p&gt;Teams with a generous paid tier might never see this bug, because the quota is large enough to absorb the amplification, which is exactly why the free tier is a better place to learn the lesson. The constraint exposes the flaw early, and fixing it on a constrained budget produces code that behaves well when the traffic eventually grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;AI-generated code is fast to produce and slow to trust, and this incident is a concrete example of where that trust needs to be earned. The fix was not more code but more restraint: a retry cap, a delay that respects the server's instructions, and a rule that validation errors never retry. The constrained setup that produced the bug also made the debugging cheap, because the free server's logs and quota told the whole story without any paid observability tooling.&lt;/p&gt;

&lt;p&gt;For anyone who wants to test this pattern without risking a production account, the workflow is to generate a webhook handler, point it at a flaky endpoint, and watch what happens under a per-minute limit. The free 10-million-token allowance and free server make that experiment cost nothing to run, and the failure mode is educational precisely because it is cheap to trigger.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>ai</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The 3 AM Job That Never Ran: Taming Sleep Mode on a Free Server</title>
      <dc:creator>Blake Yang</dc:creator>
      <pubDate>Mon, 24 Aug 2026 19:10:13 +0000</pubDate>
      <link>https://dev.to/datars_7274/the-3-am-job-that-never-ran-taming-sleep-mode-on-a-free-server-40n0</link>
      <guid>https://dev.to/datars_7274/the-3-am-job-that-never-ran-taming-sleep-mode-on-a-free-server-40n0</guid>
      <description>&lt;p&gt;At 3:00 AM, the log file should have shown a new entry, but it did not. The next night, the same silence appeared again. I had scheduled an AI-powered summary job to run on a free server, and after a few hours of inactivity, the server had quietly gone to sleep, taking my cron job with it. This is the story of how I found the problem, why it happens, and the wake-up pattern that fixed it.&lt;/p&gt;

&lt;p&gt;The server was the free tier of MonkeyCode, an open-source project that offers free model access and a free server option. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The fix, however, is not specific to MonkeyCode; it applies to any free server with a sleep policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom: A Cron Job That Skips
&lt;/h2&gt;

&lt;p&gt;The scheduled job was a simple Python script that summarized the previous day's logs and sent the result to a webhook. It was configured to run at 3:00 AM via cron, and it worked for the first two days. On the third day, the webhook received nothing, and the cron log showed no entries for the entire night. The server process was still running, and the system clock was correct, which ruled out the most obvious causes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Suspect: Timezone and Syntax
&lt;/h2&gt;

&lt;p&gt;The first step was to verify the cron configuration itself. The crontab entry used the correct server timezone, and the syntax matched the standard five-field format. A manual test of the command worked without errors. This eliminated timezone confusion and syntax mistakes, leaving the cron daemon or the server environment as the remaining suspects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Culprit: Sleep Mode
&lt;/h2&gt;

&lt;p&gt;The system logs revealed the truth: the server had entered a sleep state after roughly an hour of inactivity. Many free tiers spin down idle processes to conserve resources, and the MonkeyCode free server appears to follow the same pattern. When the server sleeps, the cron daemon is suspended, so scheduled jobs simply do not fire until the server wakes up again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wake-Up Pattern
&lt;/h2&gt;

&lt;p&gt;The solution is to keep the server awake by sending periodic HTTP requests from an external service. A cloud-based cron service such as cron-job.org can hit a lightweight endpoint on your server every few minutes. Each request wakes the server if it is sleeping, and the endpoint can also check whether the scheduled job is due and trigger it immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Minimal Flask Wake-Up Endpoint
&lt;/h2&gt;

&lt;p&gt;The following Flask application exposes a &lt;code&gt;/wake&lt;/code&gt; endpoint and runs the scheduled job in a background thread. The external cron service calls this endpoint every five minutes, which prevents the server from sleeping and also provides a reliable trigger for the 3 AM job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;job_due&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="n"&gt;JOB_HOUR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_scheduled_job&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Replace with your AI summarization logic
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Running the scheduled job at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H:%M:%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/wake&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;job_due&lt;/span&gt;
    &lt;span class="n"&gt;job_due&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;job_due&lt;/span&gt;
    &lt;span class="n"&gt;last_run_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;job_due&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localtime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tm_hour&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;JOB_HOUR&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tm_min&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;last_run_day&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tm_yday&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;run_scheduled_job&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;last_run_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tm_yday&lt;/span&gt;
            &lt;span class="n"&gt;job_due&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The external cron service should be configured to send a GET request to &lt;code&gt;https://your-server.example.com/wake&lt;/code&gt; every five minutes. The endpoint marks the job as due, and the background thread runs it exactly once per day by tracking the last run day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idempotency Trap
&lt;/h2&gt;

&lt;p&gt;A naive implementation can run the job twice if the external cron request arrives while the job is still executing. The fix is to add a lock that prevents concurrent runs. A simple file lock works for a single-process server, but a database-based lock is safer if you scale to multiple workers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;LOCK_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/scheduled_job.lock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_with_lock&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LOCK_FILE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LOCK_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;run_scheduled_job&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LOCK_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lock is not perfect, because a crash can leave a stale lock file. A more robust approach uses an atomic filesystem operation or a database row with a unique constraint. For a free-tier experiment, the simple lock is usually enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Wake-Up Pattern
&lt;/h2&gt;

&lt;p&gt;To verify the pattern, you can simulate sleep by stopping the Flask app, then start it again and send a wake request. The logs should show the job running if the request arrives within the scheduled window. A more thorough test uses a fake clock to confirm that the job runs exactly once even when the external cron service fires multiple times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Who Should Skip This
&lt;/h2&gt;

&lt;p&gt;The wake-up pattern depends on an external cron service, which adds a third-party dependency and a small amount of latency. Free servers may also limit the number of requests per minute, so a five-minute interval is usually safe, but a shorter interval could trigger rate limits. If the server completely stops rather than sleeps, the wake request may not be enough to start it, and a manual restart might be required. This approach is best for jobs that can tolerate a few minutes of delay and do not need second-level precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Free tiers are a great way to experiment with AI workloads, but they come with infrastructure quirks that are rarely documented. Sleep mode is one of those quirks, and the wake-up pattern is a simple, server-agnostic workaround. MonkeyCode's free tier includes a 10-million-token allowance and a free server option, which makes it a convenient place to test this pattern. If you are running scheduled AI jobs on a budget, the wake-up endpoint is a small piece of code that can save you a lot of missed runs.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>serverless</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Exit Code That Lied: Debugging a Silent Failure on a Free Server</title>
      <dc:creator>Blake Yang</dc:creator>
      <pubDate>Sat, 22 Aug 2026 14:21:36 +0000</pubDate>
      <link>https://dev.to/datars_7274/the-exit-code-that-lied-debugging-a-silent-failure-on-a-free-server-553g</link>
      <guid>https://dev.to/datars_7274/the-exit-code-that-lied-debugging-a-silent-failure-on-a-free-server-553g</guid>
      <description>&lt;p&gt;A scheduled sync job ran every hour on a small free server, and its logs claimed success after every single run. The target database, however, was quietly missing records that the upstream API clearly contained, which made the logs look like a deliberate lie. This article walks through that failure from the first symptom to the actual root cause, and it highlights three debugging techniques that matter more than any single fix.&lt;/p&gt;

&lt;p&gt;The job was deliberately simple in its design. A Python script pulled new records from an upstream endpoint, inserted them into a local SQLite database, and wrote a summary line to stdout for the log. Cron invoked the script through a pipeline that appended the output to a log file, and the whole thing lived on a free server with a strict time budget and a process manager that could reclaim the job at any moment.&lt;/p&gt;

&lt;p&gt;The first sign of trouble appeared when a comparison query showed a gap between the upstream record count and the local one. The log file ended with "sync completed" on every run, so the initial assumption was a data mismatch rather than a crash in the job itself. The operator asked an AI model, accessed through MonkeyCode's free model access, to review the script. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The model suggested three plausible improvements: add a retry loop with exponential backoff, print more diagnostic lines, and wrap per-record processing in a try/except so one bad record could not stop the batch.&lt;/p&gt;

&lt;p&gt;All three suggestions were reasonable in isolation, and all three made the failure harder to see in practice. The retry loop re-fetched the same broken record, the extra prints vanished into the void, and the try/except converted a crash into a silent skip. With AI-assisted debugging becoming a default workflow for many teams, the lesson here is that a model's plausible suggestion is still a hypothesis, not a verified root cause. This is the moment when the debugging retrospective really begins, because the fix was never going to come from more logging or more retries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 1: Audit the real exit code
&lt;/h2&gt;

&lt;p&gt;The cron entry looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /srv/sync &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python3 sync.py 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; sync.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pipeline returns the exit status of its last command, so the reported status always came from &lt;code&gt;tee&lt;/code&gt; rather than from Python itself. The shell keeps the real statuses in the &lt;code&gt;PIPESTATUS&lt;/code&gt; array, and reproducing the pipeline by hand exposed the lie immediately:&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;cd&lt;/span&gt; /srv/sync
python3 sync.py 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; sync.log
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pipeline exit: &lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;                 &lt;span class="c"&gt;# always 0&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"python exit:   &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PIPESTATUS&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python process had been failing for days, and the pipeline had been reporting success the entire time. Adding &lt;code&gt;set -o pipefail&lt;/code&gt; to the wrapper script would have caught this on the very first run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 2: Force unbuffered output
&lt;/h2&gt;

&lt;p&gt;The next mystery was the missing diagnostic output from the script. Python buffers stdout when it is not attached to a terminal, so prints that happened just before a crash stayed in the buffer and disappeared when the process was killed. Running the script with &lt;code&gt;python3 -u&lt;/code&gt; or setting &lt;code&gt;PYTHONUNBUFFERED=1&lt;/code&gt; revealed exactly where the job stopped, which turned out to be the first record that lacked an &lt;code&gt;external_id&lt;/code&gt; field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technique 3: Build a minimal reproducer
&lt;/h2&gt;

&lt;p&gt;The full script was too noisy for bisection, so the operator stripped it down to the smallest failing case: one upstream call, one insert, and one deliberate raise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# repro.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connecting to upstream...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing field: external_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this tiny script under the same pipeline and the same unbuffered mode confirmed the root cause in under a minute. The broad &lt;code&gt;except Exception&lt;/code&gt; that the AI review had added was swallowing the &lt;code&gt;KeyError&lt;/code&gt;, logging a single "skipped" line, and moving on as if nothing had happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause, in three layers
&lt;/h2&gt;

&lt;p&gt;The failure was not one bug but three compounding layers that reinforced each other. The pipeline masked the exit code, stdout buffering hid the last prints, and the broad exception handler converted a real error into a silent skip. The AI-suggested retry loop made the situation worse because every retry re-fetched the same broken record, and the "skipped" log line looked like progress to anyone reading the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The corrected shell wrapper checks the real exit status and fails loudly instead of hiding it:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nb"&gt;cd&lt;/span&gt; /srv/sync
python3 &lt;span class="nt"&gt;-u&lt;/span&gt; sync.py 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; sync.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corrected Python side logs the traceback and re-raises the error instead of swallowing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sync.log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%(asctime)s %(levelname)s %(message)s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record %s is missing a field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A heartbeat file gave the scheduler a way to detect stalls even when the process vanished without a trace:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# heartbeat.sh — run from cron every five minutes&lt;/span&gt;
&lt;span class="nv"&gt;last&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; %Y /srv/sync/heartbeat 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;0&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; now - last &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 300 &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sync job heartbeat expired"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The job itself touches the heartbeat file after every batch of records, so a stalled or killed run becomes visible within five minutes instead of days.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reusable checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check the real exit status of every pipeline with &lt;code&gt;PIPESTATUS&lt;/code&gt; or &lt;code&gt;set -o pipefail&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run Python with &lt;code&gt;-u&lt;/code&gt; or &lt;code&gt;PYTHONUNBUFFERED=1&lt;/code&gt; whenever output feeds a log file.&lt;/li&gt;
&lt;li&gt;Never wrap a loop body in a bare &lt;code&gt;except Exception&lt;/code&gt; that only logs a line; log the traceback and re-raise.&lt;/li&gt;
&lt;li&gt;Add a heartbeat file for long-running jobs on servers that can reclaim processes at any time.&lt;/li&gt;
&lt;li&gt;Make the job idempotent with a watermark so a re-run after a crash is safe.&lt;/li&gt;
&lt;li&gt;When an AI model suggests a fix, apply the smallest change first and confirm the failure is still visible before adding retries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this approach
&lt;/h2&gt;

&lt;p&gt;The free server is an experiment environment, not a production platform, and the constraints that made this bug visible are the same constraints that make it unsuitable for critical workloads. Teams with strict compliance or availability requirements should keep their jobs on infrastructure they control. The 10-million-token free allowance and the free server option are useful for reproductions like this one, but limits and conditions can change, so checking the current documentation before building a workflow around them is the responsible move.&lt;/p&gt;

&lt;p&gt;The debugging techniques themselves transfer everywhere, and they are the real takeaway from this retrospective. MonkeyCode is an open-source project that pairs free model access with a free server option, which makes it a practical place to run constrained-environment experiments like this one. If that kind of debugging is a regular part of your week, the project is worth a look; the free allowance makes it easy to test a hypothesis without opening a wallet.&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>bash</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Upload Endpoint Passed Every Test. Then the Security Checklist Found a Path Traversal.</title>
      <dc:creator>Blake Yang</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:02:39 +0000</pubDate>
      <link>https://dev.to/datars_7274/the-upload-endpoint-passed-every-test-then-the-security-checklist-found-a-path-traversal-3meb</link>
      <guid>https://dev.to/datars_7274/the-upload-endpoint-passed-every-test-then-the-security-checklist-found-a-path-traversal-3meb</guid>
      <description>&lt;p&gt;A file upload endpoint generated by an AI coding assistant passed every functional test in the suite. It accepted valid files, rejected oversized ones, and returned the correct status codes for each scenario. Then a fifteen-minute security checklist found a path traversal vulnerability that would have allowed an attacker to write files anywhere on the server. The endpoint was functionally correct and completely unsafe at the same time.&lt;/p&gt;

&lt;p&gt;This gap is exactly what most AI code review workflows miss. Functional tests verify that code does what it is supposed to do, and security review verifies that code does not do what it is not supposed to do. AI models are trained on patterns that look correct, and those patterns rarely include adversarial input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Functional Tests Are Not Security Tests
&lt;/h2&gt;

&lt;p&gt;A test suite that checks the happy path and a few error cases tells a developer nothing about whether an endpoint is safe. The upload endpoint had tests for valid uploads, empty files, and oversized files, and all of them passed. None of the tests used a filename like &lt;code&gt;../../etc/cron.d/evil&lt;/code&gt;, because the test author was thinking about functionality, not about what an attacker could do.&lt;/p&gt;

&lt;p&gt;Security issues live in the space between what the code does and what the code should be allowed to do. A functional test asks whether the code works, and a security check asks whether the code can be abused. These are different questions, and they require different review techniques.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Security Review Checklist for AI-Generated Code
&lt;/h2&gt;

&lt;p&gt;The following checklist takes about fifteen minutes to run on a typical endpoint. It is not a comprehensive security audit, but it catches the most common vulnerability classes in AI-generated code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Input validation: Is every user-supplied value validated against an allowlist?&lt;/li&gt;
&lt;li&gt;Path handling: Can any user input influence a file path?&lt;/li&gt;
&lt;li&gt;SQL queries: Is every query parameterized, with no string concatenation?&lt;/li&gt;
&lt;li&gt;Command execution: Can any user input reach a shell command?&lt;/li&gt;
&lt;li&gt;Authentication: Is the endpoint protected by an auth check?&lt;/li&gt;
&lt;li&gt;Authorization: Does the code verify that the user owns the resource?&lt;/li&gt;
&lt;li&gt;Error messages: Do failures leak stack traces or internal paths?&lt;/li&gt;
&lt;li&gt;Dependencies: Are all packages pinned to known versions?&lt;/li&gt;
&lt;li&gt;Rate limiting: Can an attacker hammer the endpoint without consequences?&lt;/li&gt;
&lt;li&gt;Secrets: Are any credentials hardcoded in the source?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each item on this list maps to a concrete code pattern. The path handling check, for example, looks for &lt;code&gt;open()&lt;/code&gt;, &lt;code&gt;os.path.join()&lt;/code&gt;, or &lt;code&gt;Path()&lt;/code&gt; calls that include a variable derived from request data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vulnerability in Practice
&lt;/h2&gt;

&lt;p&gt;The AI-generated upload endpoint stored files with a user-supplied filename, and the code was compact enough to look innocent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/upload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/uploads/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request with &lt;code&gt;filename=../../tmp/pwned&lt;/code&gt; would escape the uploads directory and write a file anywhere the process could write. A request with &lt;code&gt;filename=../../app/main.py&lt;/code&gt; could overwrite the application itself, depending on file permissions.&lt;/p&gt;

&lt;p&gt;The fix is to sanitize the filename and validate the extension against an allowlist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;ALLOWED_EXTENSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/upload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upload_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ALLOWED_EXTENSIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid extension&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/uploads/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Path().name&lt;/code&gt; call strips any directory components, and the allowlist rejects anything that is not an image or a PDF. The same endpoint now handles the original test cases and the adversarial ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the Triage
&lt;/h2&gt;

&lt;p&gt;A simple shell script can catch the most common vulnerability patterns in seconds, and it can run in CI on every pull request:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# security-triage.sh - quick pattern checks for AI-generated code&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== SQL string concatenation =="&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"SELECT.*(&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="s2"&gt;|f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== open() with request-derived variables =="&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"open&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;.*(request|form|args|json)"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== subprocess or os.system with variables =="&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"(subprocess|os&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;system).*(&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="s2"&gt;|f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== hardcoded secrets =="&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rnE&lt;/span&gt; &lt;span class="s2"&gt;"(password|secret|api_key)&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="s2"&gt;*['&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.py"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-vE&lt;/span&gt; &lt;span class="s2"&gt;"os&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;environ|getenv"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is a triage tool, not a security review. It produces false positives, and it misses anything that does not match a pattern. What it does well is catching the obvious mistakes that AI models make with surprising regularity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Free Tier Fits
&lt;/h2&gt;

&lt;p&gt;MonkeyCode's free model access was used to generate the original upload endpoint, and its free server option deployed it for testing. The same free model access can review the code with a security-focused prompt, and the review is useful as a second pair of eyes. The model flagged the unsanitized filename as a potential issue, though it did not explain the full attack chain, and the confirmation came from the checklist and a manual trace of the request path.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;The free server also makes the CI triage script practical for side projects and small teams. A scan that runs in seconds and costs nothing is a scan that runs on every pull request, and a scan that runs on every pull request catches issues before they reach production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;This checklist covers common web application vulnerabilities, and it does not cover business logic flaws, race conditions, or issues that require deep domain knowledge. The automated script is pattern-based, which means it misses anything that does not match a known pattern and flags some things that are not vulnerabilities. A checklist and a grep script are a floor, not a ceiling.&lt;/p&gt;

&lt;p&gt;The approach also assumes that the reviewer understands the vulnerability classes well enough to evaluate the checklist results. Someone who does not know what path traversal is will not be able to judge whether the code is vulnerable, and a checklist cannot teach that knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Should Not Use This Approach
&lt;/h2&gt;

&lt;p&gt;Teams building payment systems, healthcare platforms, or other high-risk applications should not rely on a checklist and a pattern scan. Those systems need professional security review, penetration testing, and threat modeling, and no amount of free-tier tooling replaces that. The checklist is for developers who currently do no security review at all, and it gives them a starting point that is better than nothing.&lt;/p&gt;

&lt;p&gt;The upload endpoint is still in production, and the security scan now runs on every pull request. The vulnerability was found by a fifteen-minute checklist, and the fix was three lines of code. That is the pattern that matters: most AI-generated code has common, predictable vulnerabilities, and common vulnerabilities can be caught with common checks.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>MiniMax H3 Buzz? I'd Rather Keep a Free Model on a Short Leash</title>
      <dc:creator>Blake Yang</dc:creator>
      <pubDate>Fri, 14 Aug 2026 12:03:29 +0000</pubDate>
      <link>https://dev.to/datars_7274/minimax-h3-buzz-id-rather-keep-a-free-model-on-a-short-leash-2nag</link>
      <guid>https://dev.to/datars_7274/minimax-h3-buzz-id-rather-keep-a-free-model-on-a-short-leash-2nag</guid>
      <description>&lt;p&gt;Last Tuesday, my feed was full of MiniMax H3 takes. Hot takes, cold takes, screenshot takes. I caught myself scrolling for the one magic benchmark before fixing a flaky rename script. Again.&lt;/p&gt;

&lt;p&gt;I've been burned this way before. Shiny model, zero evidence, and a half-finished eval harness from the last shiny model sitting in a folder called &lt;code&gt;evals_do_not_delete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I did something different this time. I ignored the MiniMax H3 buzz long enough to ask: what would actually change my mind?&lt;/p&gt;

&lt;p&gt;If you've read my earlier posts, you know I don't trust vibes. I want a small, reproducible loop where the model is free to run and the evidence is mine. That's where MonkeyCode's free model access and free server option became useful to me.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;The loop I keep using is boring on purpose. One task file in. One evidence file out. No dashboard, no thumbs-up button, no “look at this one impressive generation” screen capture.&lt;/p&gt;

&lt;p&gt;Here is the tiny harness I keep in the repo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;Run one coding task through a free model endpoint and keep the evidence.&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;TASK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tasks/rename_symbol.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TASK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;MODEL_CMD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MODEL_CMD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;

&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TASK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MODEL_CMD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;elapsed_seconds&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stdout&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stderr&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;returncode&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;runs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;runs/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Saved run &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json, rc=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python eval_one_task.py monkeycode-free-runner &lt;span class="nt"&gt;--free&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;monkeycode-free-runner&lt;/code&gt; is just a stand-in for whatever command points at my free endpoint. The point is not the wrapper. The point is that every run lands in &lt;code&gt;runs/&lt;/code&gt; as plain JSON.&lt;/p&gt;

&lt;p&gt;My task file looks like this:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rename_symbol_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sample_python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instruction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rename helper_function to parse_id in parser.py and update call sites."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expected_files"&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;"parser.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tests/test_parser.py"&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;Notice what the harness does not do. It does not call the model good or bad. It does not parse the output. It does not take a screenshot. It just keeps evidence.&lt;/p&gt;

&lt;p&gt;The review step stays manual. I diff the files the model touched, run the tests, and check whether anything outside &lt;code&gt;expected_files&lt;/code&gt; changed. If it did, that is a finding, not a vibe.&lt;/p&gt;

&lt;p&gt;For a lot of tasks, the free run is enough. For other tasks, it is not. I keep the decision table short.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Use free loop?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rename a symbol across a small repo&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Small blast radius, easy to diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix a flaky test in an isolated module&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Clear pass/fail signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refactor auth code with secrets&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Don't send secrets to a third-party server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generate a one-off SQL report&lt;/td&gt;
&lt;td&gt;Maybe&lt;/td&gt;
&lt;td&gt;Run only against local mock data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The free server option matters to me because reruns are cheap. I can run the same task again after a model update without burning a paid quota or my laptop battery. That is where the loop stops being a one-off demo and starts being a habit.&lt;/p&gt;

&lt;p&gt;Now about the open spirit. I keep hearing &lt;code&gt;open source&lt;/code&gt; thrown at every model release. If a maintainer wants to show me a license, great. I am not going to call MonkeyCode open source unless that license says so. What I care about is the workflow staying open: a command I can run, a free server option I can point it at, and a plain JSON trail I can diff. That is the part I do not want to give up when the MiniMax H3 threads disappear.&lt;/p&gt;

&lt;p&gt;This loop is not magic. It will not tell you that one model is &lt;code&gt;better&lt;/code&gt; than another. It will not stop a bad suggestion from entering your editor. It only removes the easiest excuses for not checking.&lt;/p&gt;

&lt;p&gt;If your code cannot leave your machine, do not send it to any free server. If you need per-request audit logs or a compliance reviewer, build that separately. If you are hoping for a leaderboard from one run, this is not the tool for you. Free access can change, so I keep the task files portable and do not let a single vendor own the format.&lt;/p&gt;

&lt;p&gt;The MiniMax H3 wave will pass. The evidence folder will still be there.&lt;/p&gt;

&lt;p&gt;If you already have a free endpoint that lets you keep your evidence outside a walled dashboard, try running the same boring task twice: once today, once after the next model drop. The diff between those two runs is the only benchmark I trust.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>testing</category>
    </item>
    <item>
      <title>My Free-Server Loop for Reviewing AI Refactors (and the First Thing It Broke)</title>
      <dc:creator>Blake Yang</dc:creator>
      <pubDate>Fri, 14 Aug 2026 10:43:45 +0000</pubDate>
      <link>https://dev.to/datars_7274/my-free-server-loop-for-reviewing-ai-refactors-and-the-first-thing-it-broke-257p</link>
      <guid>https://dev.to/datars_7274/my-free-server-loop-for-reviewing-ai-refactors-and-the-first-thing-it-broke-257p</guid>
      <description>&lt;p&gt;I keep catching myself doing the same AI review loop by hand. Ask a model to refactor a function, read the diff, run the tests, notice the patch does not apply, ask again, run the tests again. The model is not the slow part. I am the slow part, because I never wrote the loop down.&lt;/p&gt;

&lt;p&gt;So I made it boring.&lt;/p&gt;

&lt;p&gt;The point is not to trust the model more. The point is to create a cheap repeatable signal before I open a real PR. If a proposed refactor passes a baseline test suite, I will read it carefully. If it fails, I skip it and stop turning my evening into a vibes check.&lt;/p&gt;

&lt;p&gt;A lot of AI coding advice right now feels like either 'trust the agent' or 'never trust the agent.' Both are feelings. I wanted a tiny referee instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free-tier setup
&lt;/h2&gt;

&lt;p&gt;The two MonkeyCode pieces I used for this experiment are the free model access and the free server option. (Disclosure: This article was prepared as part of MonkeyCode's product outreach. I am treating those availability claims as operator-supplied, not a permanent guarantee.) The model proposes a diff. The server does the unglamorous work: apply the patch, run tests, and report whether the repo still passes.&lt;/p&gt;

&lt;p&gt;I am deliberately not calling this a benchmark. One refactor is one sample, and a green test run does not prove a change is correct. It only proves the change did not break the checks I already had.&lt;/p&gt;

&lt;h2&gt;
  
  
  The runner I copied
&lt;/h2&gt;

&lt;p&gt;This is a proposal script, not a production harness. I used a generic OpenAI-style client because many low-cost and free model paths expose one. Replace the base URL, model name, and repo path with your own values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;JOBS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;jobs.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;PROJECT_DIR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./repo&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pytest&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-q&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--disable-warnings&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MODEL_BASE_URL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MODEL_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MODEL_NAME&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;free-model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;baseline_ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;baseline ok:&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline_ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;baseline_ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseline_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JOBS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;proposed_diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ask_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;patch_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;candidate.patch&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;patch_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proposed_diff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apply&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--check&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patch_path&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&amp;gt; SKIP: patch did not apply&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apply&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patch_path&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_tests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;apply&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-R&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;patch_path&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PASS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sample &lt;code&gt;jobs.json&lt;/code&gt;:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"extract-validator"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Return only a git patch for the validate_email function in src/validators.py. Replace the nested conditionals with a clearer guard clause. Do not change public behavior."&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;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;The baseline check matters more than the model. If the repo does not pass before the model runs, the loop is meaningless. My first dry run failed because pytest discovered zero tests and exited successfully. That was not a model failure; it was a harness failure. Now I make the suite catch a deliberate failing test before I add any model work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A routing table instead of a robot
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Example change&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Variable rename, formatting&lt;/td&gt;
&lt;td&gt;Free model plus local tests&lt;/td&gt;
&lt;td&gt;Cheap and easy to revert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Multi-file behavior change&lt;/td&gt;
&lt;td&gt;Free server runner plus targeted tests&lt;/td&gt;
&lt;td&gt;The longer run can happen off my laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Auth, payment, or security-sensitive path&lt;/td&gt;
&lt;td&gt;Human review first&lt;/td&gt;
&lt;td&gt;A free model should not be the final gate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Where the free server actually helps
&lt;/h2&gt;

&lt;p&gt;The free server option matters for the long-running part. I can leave the runner pointed at a small repo, schedule it against a small queue, and let it churn through low-risk mechanical patches while I do other work. The server does not make the model smarter. It removes the excuse I usually have for not running the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and when to skip this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A free server is not a CI system. It may not have an SLA, durable storage, or enough time for large suites.&lt;/li&gt;
&lt;li&gt;Free model quotas can change. Do not send proprietary or regulated code into this loop.&lt;/li&gt;
&lt;li&gt;Passing tests is a floor, not proof. A bad suite will happily bless a bad refactor.&lt;/li&gt;
&lt;li&gt;This is triage, not model evaluation. Do not rank models from one or two patches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Who should not use this: anyone handling production secrets, compliance-heavy code, or repositories where a mistaken apply could be expensive. Start with a toy repo or a branch you can delete.&lt;/p&gt;

&lt;p&gt;If you try this, start with one mechanical refactor and deliberately make the baseline test fail once. If your runner catches it, the loop is working. Then you can go back to arguing with the model about naming, but at least the boring part is already handled.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>codequality</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Route AI Coding Tasks by Risk: A Free-Tier-First Workflow You Can Actually Measure</title>
      <dc:creator>Blake Yang</dc:creator>
      <pubDate>Thu, 13 Aug 2026 03:21:46 +0000</pubDate>
      <link>https://dev.to/datars_7274/route-ai-coding-tasks-by-risk-a-free-tier-first-workflow-you-can-actually-measure-3p50</link>
      <guid>https://dev.to/datars_7274/route-ai-coding-tasks-by-risk-a-free-tier-first-workflow-you-can-actually-measure-3p50</guid>
      <description>&lt;p&gt;Most discussions about AI coding tools start with "which model is best?" I've found that's the wrong first question. The better question is: &lt;strong&gt;which of my tasks actually need the strongest model, and which ones don't?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In my earlier posts I wrote about building a small evaluation suite for AI coding models and a falsification loop for reviewing AI-generated refactors. This post is the missing piece between them: a routing layer that decides, per task, whether a free-tier model is good enough — and a way to measure whether that decision was right, instead of trusting vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: paying frontier prices for boilerplate work
&lt;/h2&gt;

&lt;p&gt;When every prompt goes to the most expensive model by default, two things happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You burn budget on tasks a weaker model handles fine (renaming, boilerplate, docstrings, simple test generation).&lt;/li&gt;
&lt;li&gt;You never build intuition for where the strong model genuinely matters, because you never see the failure distribution of the cheap one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix isn't a blog-post benchmark. It's a per-task routing rule plus a log you can audit weekly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Classify tasks by blast radius, not difficulty
&lt;/h2&gt;

&lt;p&gt;Difficulty is subjective. Blast radius — what breaks if the output is wrong and you don't catch it — is not. I use three tiers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Task examples&lt;/th&gt;
&lt;th&gt;Failure cost&lt;/th&gt;
&lt;th&gt;Default route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Rename/refactor with compiler backing, boilerplate, doc comments, unit test scaffolding, commit message drafts&lt;/td&gt;
&lt;td&gt;Caught by compiler/CI in seconds&lt;/td&gt;
&lt;td&gt;Free/cheap model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;New function in an existing module, bug fix with a clear reproducer, small migration script&lt;/td&gt;
&lt;td&gt;Caught by code review or tests, costs an hour&lt;/td&gt;
&lt;td&gt;Free model first, escalate on failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Concurrency changes, auth/payment logic, schema migrations on live data, security-sensitive parsing&lt;/td&gt;
&lt;td&gt;May reach production silently&lt;/td&gt;
&lt;td&gt;Strongest available model + mandatory human review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rules make this table work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Escalation is cheap, so bias toward the free tier.&lt;/strong&gt; If the free model's output fails your checks, you escalate that one task. You lose minutes, not money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier High is non-negotiable.&lt;/strong&gt; Anything whose failure mode is "silent wrongness in production" never starts on the free tier, no matter how confident you feel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: A gate that every AI output must pass
&lt;/h2&gt;

&lt;p&gt;Routing only works if each tier has an objective accept/reject gate. Mine is:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# gate.sh — run after applying any AI-generated change.&lt;/span&gt;
&lt;span class="c"&gt;# Exit 0 = accept, non-zero = escalate to a stronger model or do it by hand.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== typecheck =="&lt;/span&gt;
npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt;          &lt;span class="c"&gt;# swap for: mypy, go build, cargo check...&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== existing tests =="&lt;/span&gt;
npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt;      &lt;span class="c"&gt;# must pass with zero new failures&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== diff sanity =="&lt;/span&gt;
&lt;span class="c"&gt;# Reject diffs that touch files outside the task's declared scope.&lt;/span&gt;
&lt;span class="c"&gt;# I pass the allowed path prefix as $1, e.g. ./gate.sh src/billing/&lt;/span&gt;
git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: change escaped declared scope"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scope check matters more than it looks. In my experience the most common free-tier failure isn't wrong logic — it's the model "helpfully" editing files you didn't ask about. A one-line &lt;code&gt;git diff&lt;/code&gt; filter catches that class entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Log every routed task, review weekly
&lt;/h2&gt;

&lt;p&gt;This is the part that turns routing from a superstition into a measurement. One line of JSON per task:&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="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-08-11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"free"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"gate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"escalated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6&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;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-08-11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"free"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"gate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"escalated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;19&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;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-08-11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"strong"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"gate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"escalated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"minutes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;31&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;After two weeks, answer three questions from the log:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What percentage of free-routed tasks passed the gate on the first attempt? (My threshold: if it drops below ~60% for a tier, that tier's routing rule is wrong.)&lt;/li&gt;
&lt;li&gt;When tasks escalated, did the strong model actually fix it, or was the task misclassified as Medium when it was really High?&lt;/li&gt;
&lt;li&gt;Are High-tier tasks sneaking into the free route? (Any "yes" here is a process bug, fix the table, not the model.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is deliberately the same philosophy as my earlier evaluation-suite post: small, runnable, and honest about failure counts instead of average-case impressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the free tier comes from
&lt;/h2&gt;

&lt;p&gt;Routing toward a free tier only helps if you actually have one. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode currently offers free model access and a free server option, which is what makes a free-first routing rule practical to run as an individual — the "free route" in the table above is a real default rather than a hypothetical one, and the free server means the logging/gate scripts can run somewhere other than your laptop. I won't quote specific model names, quotas, or performance numbers here, because those change and you should verify them yourself against the current offering; the workflow in this post is deliberately provider-agnostic, and the gate + log will tell you within two weeks whether the free tier is pulling its weight for &lt;em&gt;your&lt;/em&gt; codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, and who shouldn't do this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tiny codebases and solo weekend projects:&lt;/strong&gt; if you write ten AI-assisted tasks a week, the log overhead exceeds the savings. Just use whatever model and move on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domains where correctness is unverifiable by tests&lt;/strong&gt; (e.g., ML feature engineering without ground truth, UX copy): the gate script can't catch silent wrongness, so routing by gate results gives false confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regulated or security-critical code:&lt;/strong&gt; the High tier in my table should probably be "no AI generation at all, AI-assisted review only." A routing table is not a compliance story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The classification itself is a judgment call.&lt;/strong&gt; Expect to misclassify for the first two weeks; the weekly log review exists precisely to correct that.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;"Which model is best" is a benchmark question. "Which model is sufficient for this task, and how would I know if it wasn't" is an engineering question. A blast-radius table, an objective gate, and a one-line-per-task log will answer it for your own workflow in about two weeks — and whatever free tier you route to, you'll know exactly how much it's earning its place. If you try this, I'd genuinely like to hear what your pass-rate numbers look like; that's the dataset nobody publishes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>How to Review AI-Generated Refactors: A Repeatable Falsification Loop</title>
      <dc:creator>Blake Yang</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:04:47 +0000</pubDate>
      <link>https://dev.to/datars_7274/how-to-review-ai-generated-refactors-a-repeatable-falsification-loop-4816</link>
      <guid>https://dev.to/datars_7274/how-to-review-ai-generated-refactors-a-repeatable-falsification-loop-4816</guid>
      <description>&lt;p&gt;Can you trust an AI-generated refactor without re-doing the work yourself? Yes — if you stop treating the diff as a patch to approve and start treating it as a hypothesis to falsify. The workflow below is a repeatable, scripted loop (snapshot behavior → generate in isolation → mechanical falsification → human review) that shrinks a 400-line AI diff into something your judgment can actually handle.&lt;/p&gt;

&lt;p&gt;Last month I wrote about building a free evaluation suite for comparing AI coding models. That suite answers &lt;em&gt;which model&lt;/em&gt; to trust. This article is about the next question, which turned out to be harder in practice: once you've picked a model, how do you review its output on &lt;strong&gt;real refactoring work&lt;/strong&gt; without either rubber-stamping it or re-doing the work yourself?&lt;/p&gt;

&lt;p&gt;The failure mode I kept hitting: an AI-generated refactor looks clean, passes a quick skim, and then breaks an edge case three files away. Manual review of a 400-line diff is exactly where attention fails. So I built a small, repeatable loop that treats every AI refactor as a hypothesis to falsify, not a patch to approve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop in one picture
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot behavior before the refactor.&lt;/strong&gt; Capture the current behavior as executable evidence (tests + a few golden outputs), not as a vague memory of "it worked."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate the refactor in isolation.&lt;/strong&gt; One concern per request, with an explicit diff scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run mechanical falsification.&lt;/strong&gt; Tests, type checks, and a diff-shape check that flags changes outside the declared scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only then do human review.&lt;/strong&gt; By this point the diff is smaller and already survived the cheap checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key idea is that steps 1–3 are scripted and identical every time, so the human judgment in step 4 is spent where it actually matters. This mirrors the classic advice behind &lt;a href="https://martinfowler.com/bliki/CharacterizationTest.html" rel="noopener noreferrer"&gt;characterization tests&lt;/a&gt;: pin down what the code observably does &lt;em&gt;before&lt;/em&gt; you restructure it, because a &lt;a href="https://en.wikipedia.org/wiki/Code_refactoring" rel="noopener noreferrer"&gt;refactor by definition must not change behavior&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: the falsification script
&lt;/h2&gt;

&lt;p&gt;This is the core of the workflow. It's deliberately boring shell — no framework to maintain:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# falsify-refactor.sh — run after applying an AI-generated refactor&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;SCOPE_FILE&lt;/span&gt;&lt;span class="o"&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;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;.refactor-scope&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;BASE_BRANCH&lt;/span&gt;&lt;span class="o"&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;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== 1. Test suite =="&lt;/span&gt;
npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: tests"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== 2. Type check =="&lt;/span&gt;
npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: types"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== 3. Scope check: did the diff stay inside the declared files? =="&lt;/span&gt;
&lt;span class="nv"&gt;CHANGED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;...HEAD&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;VIOLATIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qxF&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCOPE_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  OUT OF SCOPE: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;VIOLATIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VIOLATIONS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: scope"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== 4. Behavior snapshot =="&lt;/span&gt;
&lt;span class="c"&gt;# Golden outputs captured BEFORE the refactor (see below)&lt;/span&gt;
node scripts/capture-behavior.js | diff &lt;span class="nt"&gt;-u&lt;/span&gt; .golden/behavior.txt - &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: behavior drift"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS: refactor survived falsification"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before requesting the refactor, I capture the golden behavior once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
node scripts/capture-behavior.js &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .golden/behavior.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;capture-behavior.js&lt;/code&gt; is just a script that exercises the module's public API with representative inputs and prints normalized output — a poor man's characterization test for code that doesn't have full test coverage yet.&lt;/p&gt;

&lt;p&gt;The scope file is the part people skip, and it's the part that catches the most real damage. When I ask a model to "extract the validation logic from &lt;code&gt;checkout.ts&lt;/code&gt; into a pure module," I write down exactly which files may change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/checkout.ts
src/validation.ts
src/validation.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model also "helpfully" tweaks an unrelated utility, the script fails loudly instead of that change slipping through inside a big diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the free model access fits
&lt;/h2&gt;

&lt;p&gt;This loop is generation-heavy: each refactor request is cheap to &lt;em&gt;check&lt;/em&gt; but not cheap to &lt;em&gt;produce&lt;/em&gt;, and I often want two or three candidate diffs for the same task so I can compare approaches. Running that volume through a paid API adds up fast for what is essentially iterative drafting.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In my current setup I run the generation step through &lt;a href="https://monkeycode.cc" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;, which offers free access to coding models and a free server option for hosting the workflow. That combination matters here for a specific reason: the falsification script is fully automated, so I can point it at the server, batch several candidate refactors, and only look at the ones that survive. The economics of "generate three, discard two" stop mattering when the generation step is free.&lt;/p&gt;

&lt;p&gt;The important thing is that the loop doesn't depend on any particular model. If the free tier disappears tomorrow, the script doesn't change — only the generation source does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table: when a candidate diff is worth human review
&lt;/h2&gt;

&lt;p&gt;After running this loop for a few weeks, I noticed my review decisions collapsed into a small table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;Types&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&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;Review properly — worth your time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;❌ any&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Discard, don't debug the model's diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;Re-scope: ask again with a tighter prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;Interesting: either the refactor changed semantics, or your golden snapshot was wrong. Check the snapshot first.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the underrated one. About a third of my "behavior drift" failures were actually stale golden outputs, which means the loop doubles as a cheap audit of how well I understand my own code's observable behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The golden snapshot is only as good as the inputs you thought of.&lt;/strong&gt; This is characterization testing, not proof. A refactor can pass and still break an input you never captured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't work for behavior-changing work.&lt;/strong&gt; This loop is for refactors — semantics-preserving changes. Feature work needs actual tests, not diffed output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell + git diff is fragile on monorepos&lt;/strong&gt; with generated files. You'd want path filters and to exclude lockfiles from the scope check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free model access has practical limits.&lt;/strong&gt; Throughput, context size, and availability can vary, so I keep prompts narrow (one concern per request) rather than shipping whole subsystems at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who should not use this
&lt;/h2&gt;

&lt;p&gt;If your codebase already has fast, high-coverage tests, steps 1 and 4 are mostly redundant — your CI is already the falsification loop, and you just need the scope check. And if your changes are usually behavior-changing features rather than refactors, this whole framing will slow you down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it this week
&lt;/h2&gt;

&lt;p&gt;If, like me, you maintain code with patchy coverage and you're increasingly letting models do the mechanical restructuring, a scriptable falsification step is the difference between "AI-assisted refactoring" and "AI-generated risk." Here's how to start today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Copy the &lt;code&gt;falsify-refactor.sh&lt;/code&gt; script above&lt;/strong&gt; into your repo and adapt the test/type commands to your stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick one low-risk module&lt;/strong&gt; and write a &lt;code&gt;capture-behavior.js&lt;/code&gt; that exercises its public API with 5–10 representative inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run your next AI refactor through the loop&lt;/strong&gt; — with a scope file, even if it feels bureaucratic. Watch what it catches.&lt;/li&gt;
&lt;li&gt;If you want the batch-generation part without paying per candidate, &lt;a href="https://monkeycode.cc" rel="noopener noreferrer"&gt;MonkeyCode's free model access and free server&lt;/a&gt; are one low-friction way to run it — the script above works regardless of what generates the diffs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What does your review process for AI-generated diffs look like? Drop a comment — I'm especially curious how people handle the scope-creep problem on larger refactors, and I'll fold the best answers into a follow-up post.&lt;/p&gt;

</description>
      <category>aicodereview</category>
      <category>refactoring</category>
      <category>testing</category>
      <category>aicodingtools</category>
    </item>
    <item>
      <title>A Repeatable Loop for Reviewing AI-Generated Refactors Before You Merge Them</title>
      <dc:creator>Blake Yang</dc:creator>
      <pubDate>Fri, 07 Aug 2026 03:03:08 +0000</pubDate>
      <link>https://dev.to/datars_7274/a-repeatable-loop-for-reviewing-ai-generated-refactors-before-you-merge-them-2egp</link>
      <guid>https://dev.to/datars_7274/a-repeatable-loop-for-reviewing-ai-generated-refactors-before-you-merge-them-2egp</guid>
      <description>&lt;p&gt;Last month I wrote about building a free evaluation suite for comparing AI coding models. That suite answers &lt;em&gt;which model&lt;/em&gt; to trust. This article is about the next question, which turned out to be harder in practice: once you've picked a model, how do you review its output on &lt;strong&gt;real refactoring work&lt;/strong&gt; without either rubber-stamping it or re-doing the work yourself?&lt;/p&gt;

&lt;p&gt;The failure mode I kept hitting: an AI-generated refactor looks clean, passes a quick skim, and then breaks an edge case three files away. Manual review of a 400-line diff is exactly where attention fails. So I built a small, repeatable loop that treats every AI refactor as a hypothesis to falsify, not a patch to approve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop in one picture
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot behavior before the refactor.&lt;/strong&gt; Capture the current behavior as executable evidence (tests + a few golden outputs), not as a vague memory of "it worked."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate the refactor in isolation.&lt;/strong&gt; One concern per request, with an explicit diff scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run mechanical falsification.&lt;/strong&gt; Tests, type checks, and a diff-shape check that flags changes outside the declared scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only then do human review.&lt;/strong&gt; By this point the diff is smaller and already survived the cheap checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key idea is that steps 1–3 are scripted and identical every time, so the human judgment in step 4 is spent where it actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Artifact: the falsification script
&lt;/h2&gt;

&lt;p&gt;This is the core of the workflow. It's deliberately boring shell — no framework to maintain:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# falsify-refactor.sh — run after applying an AI-generated refactor&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;SCOPE_FILE&lt;/span&gt;&lt;span class="o"&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;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;.refactor-scope&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;BASE_BRANCH&lt;/span&gt;&lt;span class="o"&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;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== 1. Test suite =="&lt;/span&gt;
npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: tests"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== 2. Type check =="&lt;/span&gt;
npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: types"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== 3. Scope check: did the diff stay inside the declared files? =="&lt;/span&gt;
&lt;span class="nv"&gt;CHANGED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BASE_BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;...HEAD&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;VIOLATIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; f&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qxF&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCOPE_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  OUT OF SCOPE: &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nv"&gt;VIOLATIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
  &lt;span class="k"&gt;fi
done&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VIOLATIONS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: scope"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"== 4. Behavior snapshot =="&lt;/span&gt;
&lt;span class="c"&gt;# Golden outputs captured BEFORE the refactor (see below)&lt;/span&gt;
node scripts/capture-behavior.js | diff &lt;span class="nt"&gt;-u&lt;/span&gt; .golden/behavior.txt - &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: behavior drift"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS: refactor survived falsification"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before requesting the refactor, I capture the golden behavior once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
node scripts/capture-behavior.js &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .golden/behavior.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;capture-behavior.js&lt;/code&gt; is just a script that exercises the module's public API with representative inputs and prints normalized output — a poor man's characterization test for code that doesn't have full test coverage yet.&lt;/p&gt;

&lt;p&gt;The scope file is the part people skip, and it's the part that catches the most real damage. When I ask a model to "extract the validation logic from &lt;code&gt;checkout.ts&lt;/code&gt; into a pure module," I write down exactly which files may change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/checkout.ts
src/validation.ts
src/validation.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the model also "helpfully" tweaks an unrelated utility, the script fails loudly instead of that change slipping through inside a big diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the free model access fits
&lt;/h2&gt;

&lt;p&gt;This loop is generation-heavy: each refactor request is cheap to &lt;em&gt;check&lt;/em&gt; but not cheap to &lt;em&gt;produce&lt;/em&gt;, and I often want two or three candidate diffs for the same task so I can compare approaches. Running that volume through a paid API adds up fast for what is essentially iterative drafting.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;In my current setup I run the generation step through MonkeyCode, which offers free access to coding models and a free server option for hosting the workflow. That combination matters here for a specific reason: the falsification script is fully automated, so I can point it at the server, batch several candidate refactors, and only look at the ones that survive. The economics of "generate three, discard two" stop mattering when the generation step is free.&lt;/p&gt;

&lt;p&gt;The important thing is that the loop doesn't depend on any particular model. If the free tier disappears tomorrow, the script doesn't change — only the generation source does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision table: when a candidate diff is worth human review
&lt;/h2&gt;

&lt;p&gt;After running this loop for a few weeks, I noticed my review decisions collapsed into a small table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;Types&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&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;Review properly — worth your time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;❌ any&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Discard, don't debug the model's diff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;Re-scope: ask again with a tighter prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;Interesting: either the refactor changed semantics, or your golden snapshot was wrong. Check the snapshot first.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the underrated one. About a third of my "behavior drift" failures were actually stale golden outputs, which means the loop doubles as a cheap audit of how well I understand my own code's observable behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The golden snapshot is only as good as the inputs you thought of.&lt;/strong&gt; This is characterization testing, not proof. A refactor can pass and still break an input you never captured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't work for behavior-changing work.&lt;/strong&gt; This loop is for refactors — semantics-preserving changes. Feature work needs actual tests, not diffed output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell + git diff is fragile on monorepos&lt;/strong&gt; with generated files. You'd want path filters and to exclude lockfiles from the scope check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free model access has practical limits.&lt;/strong&gt; Throughput, context size, and availability can vary, so I keep prompts narrow (one concern per request) rather than shipping whole subsystems at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who should not use this
&lt;/h2&gt;

&lt;p&gt;If your codebase already has fast, high-coverage tests, steps 1 and 4 are mostly redundant — your CI is already the falsification loop, and you just need the scope check. And if your changes are usually behavior-changing features rather than refactors, this whole framing will slow you down.&lt;/p&gt;

&lt;p&gt;But if, like me, you maintain code with patchy coverage and you're increasingly letting models do the mechanical restructuring, a scriptable falsification step is the difference between "AI-assisted refactoring" and "AI-generated risk." If you want to try the batch-generation part without paying per candidate, MonkeyCode's free model access and free server are one low-friction way to run it — the script above works regardless of what generates the diffs.&lt;/p&gt;

&lt;p&gt;What does your review process for AI-generated diffs look like? I'm especially curious how people handle the scope-creep problem on larger refactors.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>refactoring</category>
      <category>programming</category>
    </item>
    <item>
      <title>Stop Vibes-Testing AI Coding Models: A Repeatable Evaluation Suite You Can Run for Free</title>
      <dc:creator>Blake Yang</dc:creator>
      <pubDate>Wed, 05 Aug 2026 06:55:44 +0000</pubDate>
      <link>https://dev.to/datars_7274/stop-vibes-testing-ai-coding-models-a-repeatable-evaluation-suite-you-can-run-for-free-3b3n</link>
      <guid>https://dev.to/datars_7274/stop-vibes-testing-ai-coding-models-a-repeatable-evaluation-suite-you-can-run-for-free-3b3n</guid>
      <description>&lt;p&gt;Most developers evaluate a new AI coding model the same way: open a chat, type "write a REST API", nod at the output, and either subscribe or move on. I have done this too, and it is a terrible method. The output always &lt;em&gt;looks&lt;/em&gt; competent on the first prompt, and the model's real weaknesses only show up on the fifth refactor, the ambiguous requirement, or the codebase question it answers with confident fiction.&lt;/p&gt;

&lt;p&gt;This article is a small, repeatable alternative: a fixed suite of eight prompts, a scoring rubric, and a harness that records everything so you can compare models (or the same model a month later) on evidence instead of vibes. You can run the whole thing on free tiers — I will note one option below — so cost is not an excuse to skip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one-shot prompts mislead you
&lt;/h2&gt;

&lt;p&gt;A single prompt conflates three different capabilities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fluency&lt;/strong&gt; — producing syntactically plausible code. Nearly every current model passes this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specification fidelity&lt;/strong&gt; — doing what you actually asked, including the boring constraints ("no external dependencies", "must handle empty input").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honesty under uncertainty&lt;/strong&gt; — saying "I don't know this API" instead of inventing one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fluency is what a demo measures. The other two are what determine whether the model saves you time or creates debugging debt. Your evaluation suite should probe all three separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The suite: eight prompts, three categories
&lt;/h2&gt;

&lt;p&gt;Store these as files so the suite is versioned and re-runnable. Adjust the domain to your actual work — a frontend developer should swap the systems prompts for component tasks — but keep the &lt;em&gt;categories&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;evals/
  spec_fidelity/
    01_constrained_api.md      # implement X with explicit constraints
    02_refactor_with_tests.md  # change behavior, keep tests green
    03_boring_edge_cases.md    # empty input, unicode, large input
  honesty/
    04_obscure_library.md      # asks about a niche/deprecated API
    05_ambiguous_requirement.md# under-specified task; does it ask or guess?
  workflow/
    06_debug_this.md           # broken code + failing test output
    07_explain_diff.md         # explain a non-obvious diff
    08_multi_step_plan.md      # plan a migration, then execute step 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example prompt, &lt;code&gt;01_constrained_api.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Write a Python function &lt;span class="sb"&gt;`retry_with_backoff(fn, retries, base_delay)`&lt;/span&gt;.
Constraints:
&lt;span class="p"&gt;-&lt;/span&gt; Standard library only.
&lt;span class="p"&gt;-&lt;/span&gt; Exponential backoff with full jitter.
&lt;span class="p"&gt;-&lt;/span&gt; Raise the last exception after retries are exhausted.
&lt;span class="p"&gt;-&lt;/span&gt; Include type hints and one usage example.
Do not explain the code; output code only.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The constraints are the point. A model that adds &lt;code&gt;tenacity&lt;/code&gt; as a dependency or skips jitter has failed specification fidelity even if the code runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The harness
&lt;/h2&gt;

&lt;p&gt;The harness is deliberately boring: run each prompt, save the raw response, and fill in a scorecard. Automation can check mechanical constraints (did it import a banned package? does the code parse?); judgment calls (did it ask a clarifying question?) stay manual. Here is a minimal Python sketch — label it as a starting point, adapt it to whatever API you are testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# eval_runner.py — adapt endpoint/auth to your provider
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;SUITE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RESULTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d_%H%M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RESULTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Send `text` to the model under test, return raw response.
    Replace the body with your provider&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s SDK or HTTP call.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;NotImplementedError&lt;/span&gt;  &lt;span class="c1"&gt;# wire up your model endpoint here
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mechanical_checks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_nonempty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;constrained_api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prompt_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_external_import&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import tenacity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
        &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mentions_jitter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jitter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;random&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spec_fidelity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;honesty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workflow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;prompt_file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;SUITE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;NotImplementedError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wire up run_prompt() to your model endpoint first.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_file&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;mechanical_checks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RESULTS&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;__&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prompt_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;saved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then score each response 0–2 on the rubric:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Wrong, fabricated, or ignores constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Partially correct; usable after real editing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Correct and constraint-faithful; minor polish at most&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total per model: 0–16. That number is not a benchmark of the model in general — it is a measurement of &lt;em&gt;this model on work that looks like yours&lt;/em&gt;, which is the only measurement that should drive your tooling decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually look for in each category
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Honesty prompts (04, 05)&lt;/strong&gt; are the most revealing. For &lt;code&gt;04_obscure_library.md&lt;/code&gt;, ask about a real but niche API you know well, or an older version of a popular one. A model that hallucinates function signatures here will hallucinate them in your real work, and those bugs are expensive because they compile in your head while you read them. For &lt;code&gt;05_ambiguous_requirement.md&lt;/code&gt;, the &lt;em&gt;best&lt;/em&gt; response asks one or two sharp clarifying questions before coding; a model that confidently guesses is a liability on under-specified tickets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow prompts (06–08)&lt;/strong&gt; test whether the model can operate on &lt;em&gt;your&lt;/em&gt; artifacts — a failing test log, a diff, a partial migration — rather than generating greenfield code, which is where most real usage happens after week one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it for free
&lt;/h2&gt;

&lt;p&gt;Evaluation should not require a paid subscription to the thing you are evaluating — that is backwards. Free tiers and trial credits from the major providers work, and there are also coding platforms that bundle free model access.&lt;/p&gt;

&lt;p&gt;One option I used for a run of this suite: MonkeyCode offers free model access and a free server option, which was enough to execute the eight-prompt suite end to end without touching a credit card.&lt;/p&gt;

&lt;p&gt;Disclosure: This article was prepared as part of MonkeyCode's product outreach.&lt;/p&gt;

&lt;p&gt;The harness above is provider-agnostic on purpose — point &lt;code&gt;run_prompt()&lt;/code&gt; at whatever endpoint you have, including a local model if you have the hardware for one. If you want to try the suite on MonkeyCode's free tier, the setup takes a few minutes and the results directory slots straight into the comparison step below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing runs without fooling yourself
&lt;/h2&gt;

&lt;p&gt;When you have two or more &lt;code&gt;results/&lt;/code&gt; folders (different models, or the same model across months):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Compare per prompt, not just totals.&lt;/strong&gt; A model that wins 14–12 but loses the two honesty prompts is the riskier daily driver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the diffs on prompts where scores differ.&lt;/strong&gt; The raw responses tell you &lt;em&gt;why&lt;/em&gt;; totals never do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-run before concluding.&lt;/strong&gt; A single run conflates model behavior with sampling luck. Two or three runs per prompt, with the rubric applied blindly if you can manage it, is meaningfully better.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A recent DEV discussion on why &lt;a href="https://dev.to/hexisteme/sub-agent-metrics-are-not-comparable-to-main-thread-metrics-5585"&gt;sub-agent metrics are not comparable to main-thread metrics&lt;/a&gt; makes a related point worth internalizing: numbers only mean something within the context that produced them. Your 0–16 score is valid inside your suite, your rubric, and your domain — do not treat it as a leaderboard entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and who should skip this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eight prompts is a smoke test, not a benchmark.&lt;/strong&gt; It will reliably separate "clearly bad fit" from "worth a two-week trial", but it will not rank two good models against each other with confidence. For that, you need real tasks from your issue tracker and a longer evaluation window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rubric is subjective at the boundaries.&lt;/strong&gt; The 1-vs-2 call varies between reviewers. If a team runs this, calibrate on two or three example responses together first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tiers have constraints.&lt;/strong&gt; Rate limits, queueing, or model availability can affect your runs, and a free tier may not expose the exact model you would pay for. Check what you are actually being served, and do not extrapolate latency measurements from a free server to production expectations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If your work is highly specialized&lt;/strong&gt; (embedded C, formal verification, regulated-industry code), a generic suite tells you little. Build the eight prompts from your own domain or skip the exercise and run a supervised trial on real tickets instead.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The model that demos best is rarely the model that fails best. A fixed, versioned prompt suite — even a small one — converts "I tried it and it seemed fine" into a comparable artifact you can rerun whenever a new model, a new pricing tier, or a quiet capability change lands. The suite in this article is a starting skeleton: steal the structure, replace the prompts with your own work, and keep the honesty category no matter what.&lt;/p&gt;

&lt;p&gt;If you end up publishing your own version of the suite, I would be curious which category surprised you most.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
