<?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: Paul Newell</title>
    <description>The latest articles on DEV Community by Paul Newell (@newellpaul).</description>
    <link>https://dev.to/newellpaul</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%2F172670%2Ff6157e1c-7e08-4ff5-8370-a657633a4ff2.jpeg</url>
      <title>DEV Community: Paul Newell</title>
      <link>https://dev.to/newellpaul</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/newellpaul"/>
    <language>en</language>
    <item>
      <title>Coverage Lied. Mutation Testing Told the Truth.</title>
      <dc:creator>Paul Newell</dc:creator>
      <pubDate>Tue, 30 Jun 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/newellpaul/coverage-lied-mutation-testing-told-the-truth-4f8g</link>
      <guid>https://dev.to/newellpaul/coverage-lied-mutation-testing-told-the-truth-4f8g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;When AI writes both the code and the tests, coverage only proves the same blind spot was executed twice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Every test passed. Nothing was tested.
&lt;/h2&gt;

&lt;p&gt;AI wrote 20 tests for a Laravel controller.&lt;/p&gt;

&lt;p&gt;Every endpoint was covered. Every test passed. The coverage report looked healthy.&lt;/p&gt;

&lt;p&gt;Then I ran mutation testing.&lt;/p&gt;

&lt;p&gt;95 mutants generated. 68 killed. &lt;strong&gt;27 survived.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tests ran the code. They did not test it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new failure mode: shared blind spots
&lt;/h2&gt;

&lt;p&gt;Here's the old setup: a developer writes the code, developer/QA writes the tests. Bugs live in the gap between two mental models, and that gap is doing useful work. Two people, two sets of assumptions, and the disagreements are where the bugs surface.&lt;/p&gt;

&lt;p&gt;That's changing. More and more, AI writes both the code and the tests, and the more of your workflow you hand it, the more this bites. Same model, same assumptions, same missed cases.&lt;/p&gt;

&lt;p&gt;This is the real failure mode, and it's worse than "AI writes bad tests." The agent writes tests that agree with its own misunderstanding. Writing code forces you into the weeds, where you trip over the blind spots. Reading code skims past them, so the reviewer inherits the agent's assumptions without ever discovering they were assumptions.&lt;/p&gt;

&lt;p&gt;The test suite agrees with the bug.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Mutation testing is fifty years old (Lipton 1971, DeMillo–Lipton–Sayward 1978) and there are good primers on the basics elsewhere. This post isn't one. It's about what the technique reveals now that it couldn't before: not human laziness, but agent overconfidence.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;I'm using PestPHP in a Laravel backend as it is what my current test application is written in but Mutation testing exists in every major language: mutmut for Python, Stryker for JS/TS, PIT for Java. The lesson and the survivor categories generalise.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A real Laravel 12 legacy controller. Cyclomatic complexity 13 (CC 13). The agent generated 16 Feature tests and 4 Unit tests, all passing. Then:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz81gxfi8ly0eoqmy1szr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz81gxfi8ly0eoqmy1szr.png" alt="Pest mutation testing command" width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--covered-only&lt;/code&gt; restricts mutations to lines the tests actually run. So a survivor is a covered line whose assertions wouldn't notice it breaking, not a line no test ever reached. That separates the two failures coverage smears together: code you never exercised, and code you exercised without checking. This score only judges the second. It assumes coverage is already high and asks the harder question on top.&lt;/p&gt;

&lt;p&gt;95 mutants generated. 68 killed. 27 survived.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fna2z6yevuzhhjn5bq3nu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fna2z6yevuzhhjn5bq3nu.png" alt="Pest mutation summary" width="799" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pest prints each mutation line by line with the mutator that generated it. A tick means the test suite noticed the change. A cross means it survived.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F84up6oe5c5f6xtnhxah4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F84up6oe5c5f6xtnhxah4.png" alt="Pest mutation output" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each row is one transformation applied to the source. &lt;code&gt;TernaryNegated&lt;/code&gt; flips a ternary's condition, &lt;code&gt;IfNegated&lt;/code&gt; inverts a conditional, &lt;code&gt;SmallerToSmallerOrEqual&lt;/code&gt; swaps &lt;code&gt;&amp;lt;&lt;/code&gt; for &lt;code&gt;&amp;lt;=&lt;/code&gt;. The survivor on line 25, where &lt;code&gt;&amp;lt;&lt;/code&gt; became &lt;code&gt;&amp;lt;=&lt;/code&gt; and no test failed, is exactly the date-equality blind spot below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What survived
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;th&gt;%&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Response shape (&lt;code&gt;RemoveArrayItem&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;41%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boundary equality (&lt;code&gt;&amp;lt;=&lt;/code&gt; / &lt;code&gt;&amp;lt;&lt;/code&gt; / &lt;code&gt;&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;gt;=&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logic inversion (negations, boolean and/or, &lt;code&gt;===&lt;/code&gt;/&lt;code&gt;!==&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop control flow (break/continue/foreach)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before running it I made a prediction. The survivors would cluster in the hidden closure branches phpmetrics caught and I'd missed. (phpmetrics counted CC 13. I'd hand-counted 10. The three branches I missed were a &lt;code&gt;??&lt;/code&gt; default, a short-circuit, and a closure inside &lt;code&gt;-&amp;gt;map()&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;I was wrong. The closure had two survivors. Out of twenty-seven.&lt;/p&gt;

&lt;p&gt;The real gaps were boring: response bodies, date equality, loop cardinality. Places I'd skimmed because they looked routine, which is exactly where the agent skimmed when it wrote the tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the survivors teach
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Status code assertions are not contract tests.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;assertStatus(422)&lt;/code&gt; passes whether the body is &lt;code&gt;['error' =&amp;gt; '...']&lt;/code&gt; or &lt;code&gt;[]&lt;/code&gt;. Use &lt;code&gt;assertExactJson&lt;/code&gt; or &lt;code&gt;assertJsonStructure&lt;/code&gt; on error responses. This single change kills 11 of 27 mutants here for free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Date logic needs equality boundaries.&lt;/strong&gt;&lt;br&gt;
The agent wrote past, future, and "clearly active now" cases. It never wrote &lt;code&gt;$end == $now&lt;/code&gt;. Equality on continuous values feels artificial, and that's exactly why it gets missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A loop with one fixture is not really tested.&lt;/strong&gt;&lt;br&gt;
With N≤1, &lt;code&gt;break&lt;/code&gt; and &lt;code&gt;continue&lt;/code&gt; are indistinguishable. Every &lt;code&gt;foreach&lt;/code&gt; whose order matters needs a fixture with at least two items, ordered to discriminate.&lt;/p&gt;
&lt;h2&gt;
  
  
  The agentic feedback loop
&lt;/h2&gt;

&lt;p&gt;Coverage becomes smoke. It tells you the agent didn't forget to write tests. Nothing more.&lt;/p&gt;

&lt;p&gt;Mutation testing becomes the feedback loop. Run it on the diff, before review. The agent isn't bad at writing tests, it's bad at knowing which tests matter. Mutation testing tells it, mechanically, which ones do.&lt;/p&gt;

&lt;p&gt;So I asked the agent to fix it. Same controller, same prompt, but this time with the surviving mutant list as input.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tests in suite&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutants generated&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;106&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Killed&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survivors&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mutation score&lt;/td&gt;
&lt;td&gt;71.58%&lt;/td&gt;
&lt;td&gt;89.62%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Doubling the test count took survivors from 27 to 11. Note the mutant count went &lt;em&gt;up&lt;/em&gt;: the extra fixtures covered more lines, so Pest generated 106 mutants this run instead of 95. A bigger surface, killed harder. Every category dropped except the corners.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where QA judgement still matters
&lt;/h2&gt;

&lt;p&gt;The remaining 11 were not all worth chasing.&lt;/p&gt;

&lt;p&gt;Seven of the eleven I judged to be &lt;strong&gt;equivalent mutants&lt;/strong&gt;: syntactic changes that don't actually alter behaviour. Removing the &lt;code&gt;'date'&lt;/code&gt; rule from a Laravel validator that already has &lt;code&gt;'after_or_equal:today'&lt;/code&gt; is one of them. Non-dates still fail, because &lt;code&gt;after_or_equal:today&lt;/code&gt; needs a parseable date too. Detecting equivalence is undecidable in general, so the tool can't rule these out for you. You read each one and make the call. You flag it and ignore it.&lt;/p&gt;

&lt;p&gt;The other four are &lt;strong&gt;fixture-cost prohibitive&lt;/strong&gt;. Killing them is technically possible but expensive. One survivor changes &lt;code&gt;$start &amp;gt; $now&lt;/code&gt; to &lt;code&gt;$start &amp;gt;= $now&lt;/code&gt; inside a loop. The only test that distinguishes them is a violation starting at &lt;em&gt;exactly&lt;/em&gt; &lt;code&gt;now()&lt;/code&gt;: frozen time, a multi-item fixture ordered so this case fires first, fragile under clock or timezone drift. Fifteen lines of setup for a one-character mutation that triggers at one specific second.&lt;/p&gt;

&lt;p&gt;That's the QA judgement call. Mutation testing surfaces the gap. It does not decide whether the gap is worth closing.&lt;/p&gt;

&lt;p&gt;90% is the honest stopping point. High enough to trust the suite, low enough to admit which mutants are intentionally left alive.&lt;/p&gt;
&lt;h2&gt;
  
  
  A practical &lt;code&gt;/mutate&lt;/code&gt; command
&lt;/h2&gt;

&lt;p&gt;You want this to run between the agent's first test pass and your PR review. Split it into two pieces: the work, and the stopping condition.&lt;/p&gt;

&lt;p&gt;The work is a slash command. It describes a single pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run pest --mutate on the changed classes.
Categorise survivors. Add a test for each.
Print the score and surviving mutant list.
Do not weaken the production code to kill mutants.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last line is doing real work. The easiest way to kill a mutant is to delete the code it lives in, so the command isn't allowed to touch production code, only tests.&lt;/p&gt;

&lt;p&gt;But a procedure can't teach taste. "Add a test for each survivor" is the step. &lt;em&gt;How&lt;/em&gt; you kill the mutant is the part that matters. A response-shape survivor wants &lt;code&gt;assertExactJson&lt;/code&gt;, not another &lt;code&gt;assertStatus&lt;/code&gt;. A loop survivor wants a two-item ordered fixture, not a second assertion on the same one. I keep that knowledge in a &lt;code&gt;pest-code-writer&lt;/code&gt; skill, so every pass writes tests that kill mutants the right way, not the easy way.&lt;/p&gt;

&lt;p&gt;That split is what makes the loop worth trusting. The skill knows how to write the test, &lt;code&gt;/mutate&lt;/code&gt; runs the pass and holds the guardrails.&lt;/p&gt;

&lt;p&gt;The condition belongs in &lt;code&gt;/goal&lt;/code&gt;, which exists for exactly this. You give it a completion condition and the model checks after every turn whether it's met:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/goal mutation score ≥ 85% or only equivalent mutants remain
/mutate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch: the evaluator only sees the transcript. That's why the command prints the score and the survivor list, if the number isn't on screen, the goal can't see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Mutation testing pressures the tests, not the spec. Every mutant you kill makes the suite a tighter net around today's behaviour, and says nothing about whether today's behaviour was ever right. Kill every mutant on a misread requirement and you just get the wrong answer. That gap needs a human in the loop reading the requirement, not a higher score.&lt;/p&gt;

&lt;p&gt;But in a world where AI writes both sides of the contract, "would anyone notice if this changed?" is the question coverage was pretending to answer and never could.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Coverage proves the code ran. Mutation testing proves it matters.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>qa</category>
      <category>webdev</category>
    </item>
    <item>
      <title>43 Years Later: Finishing My BBC Micro Game in Assembly</title>
      <dc:creator>Paul Newell</dc:creator>
      <pubDate>Wed, 10 Jun 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/newellpaul/43-years-later-finishing-my-bbc-micro-game-in-assembly-4fd4</link>
      <guid>https://dev.to/newellpaul/43-years-later-finishing-my-bbc-micro-game-in-assembly-4fd4</guid>
      <description>&lt;p&gt;In 1983, the first program I ever wrote appeared in the September issue of &lt;em&gt;Computer &amp;amp; Video Games&lt;/em&gt; which was the pre-eminent UK games magazine of the day.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn3b45vu38wjo8vs9187j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn3b45vu38wjo8vs9187j.jpg" alt="Computer &amp;amp; Video Games issue 23, September 1983" width="800" height="1107"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Computer &amp;amp; Video Games issue 23, September 1983&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Inside that issue, in the type-in listings section, was my game:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caterpillar&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Like most type-in games of the era, it was pretty awful. Mine was doubly so as it was my first fumbling attempt at programming, written in BBC BASIC.&lt;/p&gt;

&lt;p&gt;And like a lot of coders back then, I told myself I'd rewrite it properly in assembly one day.&lt;/p&gt;

&lt;p&gt;This is that rewrite.&lt;/p&gt;

&lt;p&gt;It's only 43 years late.&lt;/p&gt;


&lt;h2&gt;
  
  
  📖 The original
&lt;/h2&gt;

&lt;p&gt;Back in the early 80s, publishing a game didn't mean uploading to GitHub or pushing to Steam.&lt;/p&gt;

&lt;p&gt;It meant seeing your code printed across pages of a magazine and hoping someone, somewhere, would type it in correctly. And that was no easy feat, as anyone who ever tackled a multi-page listing will remember. One mistyped &lt;code&gt;DATA&lt;/code&gt; statement and the whole thing fell over.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnqcbxpw11zihiewk3chy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnqcbxpw11zihiewk3chy.png" alt="The Caterpillar type-in listing as printed in C&amp;amp;VG" width="800" height="549"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Caterpillar type-in listing as printed in C&amp;amp;VG&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The premise was simple: guide a caterpillar through a maze, collecting food and avoiding the poisonous mushrooms. About 110 lines of BASIC. It ran, it was playable-ish, and it was slow and I mean SLOW ...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg0rgp1iklrmo0hqgsbaq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg0rgp1iklrmo0hqgsbaq.gif" alt="The 1983 original in play — flat green mushrooms, all in BBC BASIC" width="500" height="332"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The 1983 original in play. Jerky movement and painfully slow in BBC BASIC&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  💾 The constraints
&lt;/h2&gt;

&lt;p&gt;The BBC Micro Model B gave you 32K of RAM, a 2 MHz 6502, and not much else. The original Caterpillar lived inside that, and the limits showed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BASIC was the bottleneck.&lt;/strong&gt; BBC BASIC was truly one of the best versions of the language but still pretty hopeless for action games.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance.&lt;/strong&gt; Scrolling was very slow even using hardware calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collision detection.&lt;/strong&gt; I remember struggling to get this right&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  ⚙️ The promise
&lt;/h2&gt;

&lt;p&gt;Even as a kid, I knew what the "proper" version would need to be coded in:&lt;/p&gt;

&lt;p&gt;6502 Assembly.&lt;/p&gt;

&lt;p&gt;I just could not get to grips with it in those early days.&lt;/p&gt;

&lt;p&gt;I did eventually become competent in 6502 a couple of years later but that was with the Commodore 64 (C64) and its hardware sprites which were way easier to get moving.&lt;/p&gt;

&lt;p&gt;I never did get back to the BBC game.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧠 43 years later
&lt;/h2&gt;

&lt;p&gt;I knew my original game was hosted in the &lt;a href="https://www.bbcmicro.co.uk/game.php?id=1066" rel="noopener noreferrer"&gt;BBC Micro Archive&lt;/a&gt; and it always nagged me that I was never able to make the game I wanted back in the day.&lt;/p&gt;

&lt;p&gt;So this wasn't just a translation. It was adding all the things I wanted to do on the first version but could not fit in.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6502 assembly handles the game.&lt;/strong&gt; Sprites, scrolling, collision and the season cycle, in MODE 2 (16 colours), running on a vsync-locked 50Hz loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BASIC runs &lt;code&gt;CALL &amp;amp;1900&lt;/code&gt; to hand control to the engine; the engine plays the game, then returns a result code and the score in zero page for BASIC to display.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔍 1983 vs 2026
&lt;/h2&gt;

&lt;p&gt;Although they look similar, they are quite different under the hood.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardware scrolling, kept.&lt;/strong&gt; Both versions scroll the easy way: a &lt;code&gt;VDU 30, 11&lt;/code&gt; at the top of the screen makes the MOS nudge the CRTC display-start registers, so the hardware does the work for almost free. The 1983 version already got this right, so the rewrite kept it. I did consider writing directly to the CRTC's R12/R13 registers itself instead of going through the MOS, games like Repton did this, but I backed out of that pretty quickly due to the added complexity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct sprite blits.&lt;/strong&gt; Each sprite is pre-encoded and written straight to screen memory, four bytes per scanline. No slow BASIC, no OS character calls in the path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ring-buffer collision.&lt;/strong&gt; Items are tracked in a 24-slot ring as they scroll from the top of the screen down to the caterpillar's row, so a "hit" is just a position lookup, with no per-pixel scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic by design.&lt;/strong&gt; The original just splatted random mushrooms on the screen but this one has a proper map like any other speed runner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score Attack.&lt;/strong&gt; The new version finishes at 4 seasons. Score is everything - grab what you can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Game mechanics.&lt;/strong&gt; Hot streaks, bonus rounds and an easter egg 🥚&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I could not grasp in 1983 was that MODE 2 memory is laid out in character cells of 8 bytes. So stepping down one pixel row is usually just &lt;code&gt;+1&lt;/code&gt;, until you fall off the bottom of a cell and have to leap &lt;code&gt;+633&lt;/code&gt; bytes to land on the next character row down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.advance_scanline
    INC temp0
    LDA temp0 : AND #7        ; still inside this character cell?
    BNE as_same_row           ; yes next byte is just +1
    CLC                       ; no cross to the next character row
    LDA scr_addr_lo : ADC #LO(633) : STA scr_addr_lo
    LDA scr_addr_hi : ADC #HI(633) : STA scr_addr_hi
.as_same_row
    INC scr_addr_lo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one odd number, 633, is the whole quirk of BBC screen memory in a nutshell. No wonder I struggled with this at the time and why I found C64 sprites so much easier.&lt;/p&gt;

&lt;p&gt;The original was ~110 lines of BASIC. The rewrite is around 1,250 lines of lightly commented 6502, which assembles down to just &lt;strong&gt;3 KB&lt;/strong&gt; of machine code.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎮 Seeing it run again
&lt;/h2&gt;

&lt;p&gt;There's something uniquely satisfying about seeing old code come back to life.&lt;/p&gt;

&lt;p&gt;Not really modernised. Just finished, the way I always meant to write it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxb4z8y7ki867quy17zh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxb4z8y7ki867quy17zh.gif" alt="The 2026 rewrite — the same game in native 6502, with proper multi-colour sprites" width="500" height="346"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The 2026 rewrite — the same game in native 6502, fast and fluid sprites&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🏆 Score to beat
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Will Newell - 4605&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ⏳ Why this mattered
&lt;/h2&gt;

&lt;p&gt;This was never really about optimisation.&lt;/p&gt;

&lt;p&gt;It was about finishing something.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If I say I'll do something, I'll do it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Some things just take a little longer than expected 😄&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;BBC Micro Archive (the 1983 original): &lt;a href="https://www.bbcmicro.co.uk/game.php?id=1066" rel="noopener noreferrer"&gt;https://www.bbcmicro.co.uk/game.php?id=1066&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/newell-paul/caterpillar-assembler" rel="noopener noreferrer"&gt;https://github.com/newell-paul/caterpillar-assembler&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Play in the browser: &lt;a href="https://newell-paul.github.io/caterpillar-assembler/" rel="noopener noreferrer"&gt;https://newell-paul.github.io/caterpillar-assembler/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>retrocomputing</category>
      <category>assembly</category>
      <category>gamedev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Claude Code Has a Power Meter. You Just Have to Wire It Up</title>
      <dc:creator>Paul Newell</dc:creator>
      <pubDate>Tue, 02 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/newellpaul/claude-code-has-a-power-meter-you-just-have-to-wire-it-up-386g</link>
      <guid>https://dev.to/newellpaul/claude-code-has-a-power-meter-you-just-have-to-wire-it-up-386g</guid>
      <description>&lt;p&gt;&lt;em&gt;One bash script. Your session cost in dollars, live, so you stop burning Opus on work that doesn't need it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj1wbdl9q8xpaj28n25rs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj1wbdl9q8xpaj28n25rs.png" alt="HUD with subset of segments" width="798" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I kept leaving sessions on Opus long after the hard thinking was done. Claude Code does warn you, but the context and rate-limit notices flash past and you dismiss them without reading. Discipline loses to a number you can't see.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Grab &lt;code&gt;statusline-hud.sh&lt;/code&gt; from &lt;strong&gt;&lt;a href="https://github.com/newell-paul/statusline-hud" rel="noopener noreferrer"&gt;github.com/newell-paul/statusline-hud&lt;/a&gt;&lt;/strong&gt;, drop it in &lt;code&gt;~/.claude/&lt;/code&gt;, &lt;code&gt;chmod +x&lt;/code&gt; it.&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;statusLine&lt;/code&gt; block to &lt;code&gt;~/.claude/settings.json&lt;/code&gt; (snippet below).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Wire it up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jq&lt;/code&gt; on your &lt;code&gt;PATH&lt;/code&gt;. The script hard-exits with &lt;code&gt;⚠ jq missing&lt;/code&gt; without it.&lt;/li&gt;
&lt;li&gt;A terminal with 256-colour and UTF-8 support, and a font that renders &lt;code&gt;🔥 ⚡ ↺ ↩ ✗ ░ █ ▏▎▍▌▋▊▉&lt;/code&gt;. Any modern terminal with a Nerd Font, or the macOS/iTerm2/GNOME defaults, will do.&lt;/li&gt;
&lt;li&gt;macOS or Linux. Native Windows isn't supported; the script is bash and uses POSIX tools (WSL should work, untested).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Install&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy &lt;code&gt;statusline-hud.sh&lt;/code&gt; to &lt;code&gt;~/.claude/&lt;/code&gt; and make it executable.&lt;/li&gt;
&lt;li&gt;Add this to &lt;code&gt;~/.claude/settings.json&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&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;"statusLine"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/.claude/statusline-hud.sh"&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;&lt;code&gt;refreshInterval&lt;/code&gt; is optional. Leave it out and the bar redraws on Claude Code events: each new assistant message, after &lt;code&gt;/compact&lt;/code&gt;, and on permission-mode or vim-mode changes. Add &lt;code&gt;"refreshInterval": 5&lt;/code&gt; if you want it to also tick on a timer, for example to keep the countdown accurate while background subagents are running.&lt;/p&gt;

&lt;p&gt;The repo has 80 bats tests and a hardened &lt;code&gt;git_safe()&lt;/code&gt; wrapper that disables &lt;code&gt;core.fsmonitor&lt;/code&gt; and &lt;code&gt;core.hooksPath&lt;/code&gt;, so a hostile &lt;code&gt;.git/config&lt;/code&gt; can't execute code via the bar.&lt;/p&gt;

&lt;p&gt;Settings reload; the bar appears on your next interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The JSON hiding in the statusline
&lt;/h2&gt;

&lt;p&gt;Claude Code's statusline is just a shell command: JSON in on stdin, one line of text out. Cost, effort, fast mode, cache stats, both rate-limit windows are all already in there, emitted on every render (debounced 300ms). Nothing's missing. There's just nothing showing it. (Tap the pipe with &lt;code&gt;tee&lt;/code&gt; to see the full payload; PAYG (pay-as-you-go) and API users will find &lt;code&gt;rate_limits&lt;/code&gt; absent.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What the bar shows
&lt;/h2&gt;

&lt;p&gt;With everything switched on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Directory + git status (&lt;code&gt;↑N↓N&lt;/code&gt;, &lt;code&gt;✗&lt;/code&gt; if dirty)&lt;/li&gt;
&lt;li&gt;Model name with effort badge (&lt;code&gt;⚡Lo&lt;/code&gt; / &lt;code&gt;⚡Med&lt;/code&gt; / &lt;code&gt;⚡Hi&lt;/code&gt; / &lt;code&gt;⚡xHi&lt;/code&gt; / &lt;code&gt;⚡Max&lt;/code&gt;) and fast-mode rocket 🚀 when &lt;code&gt;/fast&lt;/code&gt; is active&lt;/li&gt;
&lt;li&gt;Context-window bar: 5 cells using eighths (&lt;code&gt;▏▎▍▌▋▊▉█&lt;/code&gt;) so the bar moves smoothly within a tier, with the tier colour stepping green → yellow → orange → red (see the README for the exact ctx and rate-limit breakpoints; they differ)&lt;/li&gt;
&lt;li&gt;5-hour and 7-day rate-limit bars (both shown by default; comment either out in &lt;code&gt;SEGMENTS&lt;/code&gt;). A reset countdown (&lt;code&gt;↺2h14m&lt;/code&gt;) attaches to whichever limit is more constrained, and only once it crosses 60%&lt;/li&gt;
&lt;li&gt;Cache hit ratio (&lt;code&gt;↩97%&lt;/code&gt;), only after &amp;gt;5k input tokens&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;🔥&lt;/code&gt; cumulative session spend in USD (or input tokens; see toggle below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is configurable from the &lt;code&gt;CONFIG&lt;/code&gt; block at the top of the script.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjnseh3c207ev1m35z291.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjnseh3c207ev1m35z291.png" alt="Segments array" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The &lt;code&gt;SEGMENTS&lt;/code&gt; array is your control panel. Comment out a line to hide that segment, reorder lines to rearrange the bar. Want git status on the right and the flame on the left? Move the lines.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Other knobs you'll probably touch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TURN_UNIT&lt;/code&gt;: &lt;code&gt;usd&lt;/code&gt; (default) shows the flame in dollars; flip to &lt;code&gt;tokens&lt;/code&gt; for input-token count instead. In &lt;code&gt;tokens&lt;/code&gt; mode the value is the &lt;em&gt;live context window&lt;/em&gt; (see the flame section below), so its thresholds are fractions of a 200k context, not session totals.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TIER_COLOR&lt;/code&gt;, &lt;code&gt;BAR_CTX&lt;/code&gt;, &lt;code&gt;BAR_LINEAR&lt;/code&gt;: bar palette and the tier thresholds at which each bar flips colour.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TURN_HI_USD&lt;/code&gt; / &lt;code&gt;TURN_MED_USD&lt;/code&gt;: USD thresholds for the flame's amber/red (&lt;code&gt;TURN_HI_TOK&lt;/code&gt; / &lt;code&gt;TURN_MED_TOK&lt;/code&gt; for tokens mode).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One &lt;code&gt;jq&lt;/code&gt; call and a few &lt;code&gt;git&lt;/code&gt; invocations per render. Cheap enough to ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flame: a session-scale spend gauge
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;🔥 $5.64&lt;/code&gt; is the cumulative session spend, read straight from &lt;code&gt;cost.total_cost_usd&lt;/code&gt;. Green under $5, amber $5–$20, red ≥ $20, tuned for Max users where a long Opus session runs that high in estimated spend. Red doesn't mean stop. It means &lt;em&gt;the next routine task is the one to drop to Sonnet for, or &lt;code&gt;/clear&lt;/code&gt; and start fresh&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;PAYG users paying list price should dial down: try &lt;code&gt;TURN_MED_USD=0.50&lt;/code&gt; / &lt;code&gt;TURN_HI_USD=2.00&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The discomfort isn't financial, it's &lt;em&gt;visible&lt;/em&gt;. Same psychology as a power meter: you don't read the number, you notice when it pegs into the red.&lt;/p&gt;

&lt;p&gt;Green means I haven't checked the bar in a while and don't need to. Amber means the session has grown legs, fine if I meant it, worth noticing if I didn't. Red means I'm running Opus on a long, heavy context, and it's time to make a choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Acting on the red
&lt;/h2&gt;

&lt;p&gt;Opus for architecture, complex debugging, ugly refactors. Sonnet for most everyday work. Haiku for routine commands.&lt;/p&gt;

&lt;p&gt;On Pro and Max, &lt;code&gt;cost.total_cost_usd&lt;/code&gt; is an estimate, not your bill: API list rates applied client-side, which can differ from what you're actually billed. It's still the best signal for what the flame answers, which is how much have I put through the model so far.&lt;/p&gt;

&lt;p&gt;If you'd rather see input tokens, flip &lt;code&gt;TURN_UNIT=tokens&lt;/code&gt; in the CONFIG block, but be aware that as of v2.1.132 &lt;code&gt;total_input_tokens&lt;/code&gt; reflects &lt;em&gt;current&lt;/em&gt; context window, not cumulative session totals, and can drop after a &lt;code&gt;/compact&lt;/code&gt;. The default token thresholds (&lt;code&gt;TURN_MED_TOK&lt;/code&gt; / &lt;code&gt;TURN_HI_TOK&lt;/code&gt;) are tuned to that: fractions of a 200k context, so red means "context is filling up," not "expensive session."&lt;/p&gt;

&lt;p&gt;For historical totals, &lt;code&gt;/usage&lt;/code&gt; gives you the deeper view. The bar nudges you in the moment; &lt;code&gt;/usage&lt;/code&gt; is the rear-view mirror.&lt;/p&gt;

&lt;p&gt;Once the bar is in front of you, two habits move the needle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pin routine slash commands to &lt;code&gt;model: haiku&lt;/code&gt;. Commit, lint, review-diff: none of these need Opus.&lt;/li&gt;
&lt;li&gt;Use subagents for delegation. They get a fresh, isolated context window, not your full history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a session has done its job, &lt;code&gt;/clear&lt;/code&gt; before the next task so you're not dragging a stale context forward; if you want to keep the thread, &lt;code&gt;/compact&lt;/code&gt; reclaims the space instead. Then swap to the model the next task actually needs.&lt;/p&gt;




&lt;p&gt;The data was always there. Now you can see it.&lt;/p&gt;




&lt;p&gt;The repo is at &lt;strong&gt;&lt;a href="https://github.com/newell-paul/statusline-hud" rel="noopener noreferrer"&gt;github.com/newell-paul/statusline-hud&lt;/a&gt;&lt;/strong&gt;. Issues and PRs welcome.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>programming</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Programmed an AI in 6502 Assembly - It Worked</title>
      <dc:creator>Paul Newell</dc:creator>
      <pubDate>Wed, 06 May 2026 12:21:00 +0000</pubDate>
      <link>https://dev.to/newellpaul/i-programmed-an-ai-in-6502-assembly-it-worked-gpi</link>
      <guid>https://dev.to/newellpaul/i-programmed-an-ai-in-6502-assembly-it-worked-gpi</guid>
      <description>&lt;p&gt;In 1975 the 6502 processed 8-bit values through memory and control flow. A Claude Code skill now uses the same mnemonics to process forge tickets through triage loops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paul Newell&lt;/strong&gt; · Apr 2026 · 8 min read&lt;/p&gt;




&lt;p&gt;I'll be honest, this started as a joke. I wanted to see if I could get Claude Code to understand 6502 assembly language. &lt;/p&gt;

&lt;p&gt;The 6502 powered the Apple II, the Commodore 64, the BBC Micro, the NES. It had 56 opcodes, 64KB of address space and absolutely no business anywhere near a large language model.&lt;/p&gt;

&lt;p&gt;So I built a Claude Code skill that maps its instruction set onto a modern triage-and-fix workflow. You write &lt;code&gt;.s&lt;/code&gt; files. Claude executes them. Its response is also assembly. There's a zero-page. There's a stack. &lt;code&gt;BRK&lt;/code&gt; means "commit and halt."&lt;/p&gt;

&lt;h2&gt;
  
  
  Same Verbs, Bigger Nouns
&lt;/h2&gt;

&lt;p&gt;Forty years later I'm writing LDA to load issue 42, which arrives with a description, acceptance criteria, a set of labels, screenshots Claude can actually &lt;em&gt;see&lt;/em&gt; with vision and a graph of linked PRs. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The verbs haven't changed. The nouns got about six orders of magnitude bigger.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Smallest Useful Program
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj7fse810fejq252dsh2l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj7fse810fejq252dsh2l.png" alt="peek.s" width="788" height="466"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;peek.s - &lt;em&gt;6502 looking very out of place in colour&lt;/em&gt;&lt;/em&gt;&lt;br&gt;
&amp;nbsp;&lt;/p&gt;

&lt;p&gt;Six opcodes, three flags in play. This is the heart of what the whole thing does:&lt;/p&gt;

&lt;p&gt;It's not a real 6502, of course. I just borrowed the mnemonics.&lt;/p&gt;

&lt;p&gt;If the tests pass (&lt;code&gt;C=1&lt;/code&gt;), commit and halt. If they fail (&lt;code&gt;C=0&lt;/code&gt;), branch to &lt;code&gt;skip&lt;/code&gt; and return without committing. That's it. The &lt;code&gt;BCC&lt;/code&gt;/&lt;code&gt;BCS&lt;/code&gt; pattern carries over directly from the real 6502. Carry set means success, carry clear means failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trace Is the Reply
&lt;/h2&gt;

&lt;p&gt;An 800-token prose response ("Let me analyse this issue. I'll start by examining the file at...") becomes a handful of lines of assembly trace.&lt;/p&gt;

&lt;p&gt;Issue fetched, file fixed, tests passed, diff reviewed, committed, PR opened. No "Let me now...", no "I'll proceed to...", no "Here's what I did...". Just the trace.&lt;/p&gt;

&lt;p&gt;That trace is structured. Diffable. Greppable. Re-runnable. Whether it costs fewer tokens than prose turned out to be more complicated than I expected, but the structural value is still there.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Shot, One Fix
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;oneshot.s&lt;/code&gt; is the next step up: fetch an issue, analyze it, fix it, test it, self-review, and commit, with a single retry branch if the first attempt fails.&lt;/p&gt;

&lt;p&gt;If the first fix passes tests and review, &lt;code&gt;BRK&lt;/code&gt; commits immediately. If not, the retry branch gives it one more shot. If that also fails, &lt;code&gt;RTS&lt;/code&gt; leaves the branch uncommitted for a human to pick up. The carry flag is doing exactly what it did on the original chip,  routing control flow on a binary result.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgoogz5aichu7hd32m3w7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgoogz5aichu7hd32m3w7.png" alt="oneshot.s" width="800" height="640"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;oneshot.s - that's better, much easier on the eyes&lt;/em&gt;&lt;br&gt;
&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works: 15 Opcodes and 9 Vectors
&lt;/h2&gt;

&lt;p&gt;The skill is called &lt;strong&gt;opcode&lt;/strong&gt; and it defines a semantic DSL that borrows the 6502's mnemonics but maps them onto issue triage. The core ISA is just 15 opcodes. You can genuinely hold the entire thing in your head.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LDA&lt;/code&gt; loads an issue. &lt;code&gt;STA&lt;/code&gt; persists to a slot. &lt;code&gt;LDX&lt;/code&gt;/&lt;code&gt;INX&lt;/code&gt;/&lt;code&gt;CPX&lt;/code&gt; handle loops. &lt;code&gt;JSR&lt;/code&gt; calls one of nine I/O vectors: &lt;code&gt;FETCH&lt;/code&gt;, &lt;code&gt;FIX&lt;/code&gt;, &lt;code&gt;TEST&lt;/code&gt;, &lt;code&gt;LINT&lt;/code&gt;, &lt;code&gt;REVIEW&lt;/code&gt;, &lt;code&gt;PUSH&lt;/code&gt;, &lt;code&gt;PULL&lt;/code&gt;, &lt;code&gt;CLONE&lt;/code&gt;, &lt;code&gt;ANALYZE&lt;/code&gt;. &lt;code&gt;BCC&lt;/code&gt;/&lt;code&gt;BCS&lt;/code&gt; branch on test results. &lt;code&gt;BRK&lt;/code&gt; commits and halts. &lt;code&gt;PHA&lt;/code&gt;/&lt;code&gt;PLA&lt;/code&gt; push and pop todos from a stack that maps directly to Claude Code's task list.&lt;/p&gt;

&lt;p&gt;There's a zero-page with named slots. &lt;code&gt;$00&lt;/code&gt; is the current issue ID, &lt;code&gt;$01&lt;/code&gt; is the branch name, &lt;code&gt;$02&lt;/code&gt; is the current file, &lt;code&gt;$05&lt;/code&gt; is the commit message. The fetched issue queue lives at &lt;code&gt;$10-$1F&lt;/code&gt;. The stack page at &lt;code&gt;$0100-$01FF&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; the Claude Code task list.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Opcode Oriented Programming (OOP, obviously 🙂)&lt;/p&gt;

&lt;p&gt;6502 mnemonics. Modern AI execution. Workflows as programs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It talks to GitHub or GitLab through a forge abstraction layer so &lt;code&gt;JSR PUSH&lt;/code&gt; opens a PR on GitHub or an MR on GitLab. Nobody has to compromise their vocabulary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Real Triage Session
&lt;/h2&gt;

&lt;p&gt;This program loops through every issue labelled bug, tries to fix each one and only commits the ones where tests pass and the self-review is clean. Failed fixes get skipped. At the end it opens PRs for everything that worked. The &lt;code&gt;PHA&lt;/code&gt;/&lt;code&gt;PLA&lt;/code&gt; cycle maps directly onto Claude Code's task list so each pushed issue becomes a real tracked todo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl2hrws5b6bfl3brbpyj5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl2hrws5b6bfl3brbpyj5.png" alt="drain-the-swap.s" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;drain-the-swamp.s - in its preferred output format; complete with historically accurate code review comments&lt;/em&gt;&lt;br&gt;
&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Constraint Is the Feature
&lt;/h2&gt;

&lt;p&gt;The 6502 aesthetic isn't just nostalgic decoration, it's a forcing function. When your output format is assembly you commit to a verb sequence before execution instead of wandering through chat mode. The resulting traces scan way faster than prose. They're diffable, greppable and versionable. You can commit a &lt;code&gt;.s&lt;/code&gt; file alongside your code and re-run it on the next batch of issues.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The temptation to add "just one sentence of context" is the whole failure mode. Resist it absolutely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The skill is pretty militant about this. Claude's response must be a valid &lt;code&gt;.s&lt;/code&gt; program. No greetings, no sign-offs, no "Let me", no markdown headers. If it doesn't fit as an instruction, a directive or a semicolon comment it doesn't get said. There are escape hatches like &lt;code&gt;.ASK&lt;/code&gt; for questions (≤60 chars), &lt;code&gt;.NOTE&lt;/code&gt; for observations (≤80 chars) and &lt;code&gt;.ERR&lt;/code&gt; for errors. But these are pressure-release valves, not invitations to prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Extended ISA: For Completeness and Nostalgia
&lt;/h2&gt;

&lt;p&gt;There's a full extended layer (~40 more mnemonics) you can opt into with &lt;code&gt;.EXTENDED ON&lt;/code&gt;. Shifts become priority promotions (&lt;code&gt;ASL A&lt;/code&gt; = "promote priority"). Logic ops become label-filter algebra (&lt;code&gt;AND #imm&lt;/code&gt; = "intersect label mask"). &lt;code&gt;BVS&lt;/code&gt; branches on merge conflict. Most of these are tagged as metaphor so they get narrated in the trace but not actually executed.&lt;/p&gt;

&lt;p&gt;And for completeness there are six illegal opcodes behind &lt;code&gt;.UNSAFE ON&lt;/code&gt;. The real undocumented 6502 ops like &lt;code&gt;LAX&lt;/code&gt;, &lt;code&gt;SAX&lt;/code&gt; and &lt;code&gt;DCP&lt;/code&gt;. Pure flavour. Narrated, never executed, not even with the directive set. They exist simply because I could not resist adding them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about tokens?
&lt;/h2&gt;

&lt;p&gt;I expected the tight assembly output to translate to substantial cost savings versus prose Claude. The story turned out more nuanced.&lt;/p&gt;

&lt;p&gt;The reason is asymmetric overhead. For small, deterministic tasks, the orchestration overhead dominates the work — like running a CI pipeline to compile a single file. Opcode comes out ahead on larger programs with more decision points (multi-issue queue walks, longer reasoning chains where prose Claude would actually ramble).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prose prompts invite interpretation. Assembly doesn't.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  So Is This Actually Useful?
&lt;/h2&gt;

&lt;p&gt;Maybe. if you're running the same triage pattern across many issues like scanning a backlog, fixing straightforward bugs and opening PRs then the token savings start making sense and the structured output is way tighter to read than endless TL;DR prose. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.s&lt;/code&gt; files are reviewable artifacts. &lt;/p&gt;

&lt;p&gt;The traces are audit logs. &lt;/p&gt;

&lt;p&gt;And the constraint of planning your workflow as a sequence of opcodes before execution turns out to be surprisingly good discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  BRK
&lt;/h2&gt;

&lt;p&gt;At the end of the day the question is the same one it's always been. &lt;/p&gt;

&lt;p&gt;Are you a quiche eater or a Real Programmer?&lt;/p&gt;

&lt;p&gt;Real Programmers don't need prose. &lt;/p&gt;

&lt;p&gt;They need opcodes, a carry flag and a &lt;code&gt;BRK&lt;/code&gt; at the end.&lt;/p&gt;

&lt;p&gt;Commit the work. Halt the machine 🙂&lt;/p&gt;




&lt;p&gt;Somewhere between &lt;code&gt;JSR FETCH&lt;/code&gt; and &lt;code&gt;BRK&lt;/code&gt; this stopped feeling entirely satirical &lt;/p&gt;

&lt;p&gt;But if this article raised a smile or jogged some long-distant memories then please comment below.&lt;/p&gt;




&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;Full skill and example programs: &lt;a href="https://github.com/newell-paul/opcode" rel="noopener noreferrer"&gt;github.com/newell-paul/opcode&lt;/a&gt;&lt;/p&gt;




</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
