<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://www.petervanonselen.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.petervanonselen.com/" rel="alternate" type="text/html" /><updated>2026-05-11T13:44:55+00:00</updated><id>https://www.petervanonselen.com/feed.xml</id><title type="html">Peter van Onselen — Staff Engineering &amp;amp; AI</title><subtitle>A staff engineer figuring out AI-assisted development in public.</subtitle><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><entry><title type="html">An Army of One Prompt</title><link href="https://www.petervanonselen.com/2026/05/10/an-army-of-one-prompt/" rel="alternate" type="text/html" title="An Army of One Prompt" /><published>2026-05-10T08:00:00+00:00</published><updated>2026-05-10T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/05/10/an-army-of-one-prompt</id><content type="html" xml:base="https://www.petervanonselen.com/2026/05/10/an-army-of-one-prompt/"><![CDATA[<p><em>On discovering that good process survives the jump from code to plastic.</em></p>

<hr />

<p><img src="/assets/army-of-prompts/hero.jpg" alt="hero" /></p>

<p>Three days. Evenings only. Not particularly stressed about it.</p>

<p>That is how long it took to go from nothing to a printed 1000 point Soviet army for Bolt Action. Roughly fifty models, around forty of them unique sculpts, fully designed, generated, modelled, sliced, printed, and sitting on my desk. Concept to physical thing in my hand, three evenings, in my spare time, while doing everything else I usually do.</p>

<p>That is … insane. I want to write this down because I think it is genuinely insane and I want to figure out on the page how it happened, because the <em>how</em> is the part I find more interesting than the <em>what</em>.</p>

<p><img src="/assets/army-of-prompts/soviets.jpg" alt="soviet army" /></p>

<h2 id="a-quick-recap-for-anyone-arriving-cold">A quick recap, for anyone arriving cold</h2>

<p>A couple of weeks ago I wrote about <a href="https://www.petervanonselen.com/2026/04/22/vibe-coding-reality/">vibe coding a model into my house</a>. That was the moment when a chain of generative tools produced a 3D printed World War II infantryman on my desk and quietly broke my brain. That post was about the astonishment. This one is about what happened when I stopped being astonished and started actually trying to build something at scale.</p>

<p>The original plan was two armies at 500 points each for Bolt Action v3. Thanks to wildly over achieving and the madness that only comes with AI enhanced momentum … the plan became somewhat more. Germans and Soviets, a thousand points each, scaled down to centimetres so they fit on a kitchen table instead of a garage floor. The toolchain was simple. ChatGPT for concept images, Meshy to turn front and back images into 3D models, Bambu Studio to prepare them for printing, and a Bambu printer to make them real. The Germans came first. The Germans were an education …</p>

<h2 id="the-german-army-taught-me-everything-by-being-slightly-wrong">The German army taught me everything by being slightly wrong</h2>

<p>I built the Germans the way I imagine most people would on first contact with this stack. Find a reference image, drop it into Gemini, ask for a soldier in that style, take the result, drop it into Meshy, get a model, slice it, print it. Repeat for every unit.</p>

<p>The starting reference image was a Fimo clay figure I had found from a random guy on Facebook. Cute, chubby, slightly weird proportions. I liked it. I started by feeding that image to Gemini and going “more of these, but Germans.” Somewhere along the way I started telling it I wanted that mixed with Metal Slug. The chunky comic-game vibe, oversized weapons, exaggerated everything. That was the visual register I was reaching for.</p>

<p>It mostly worked. The models came out. They were even, broadly, recognisable as German infantry. But every one of them was slightly off in a different way. The proportions drifted between models. The base treatment changed. One had a helmet that read as a beret. Another had a rifle thinner than the soldier’s wrist, which would have snapped off the print bed if I had even looked at it sideways. It was a constant fight with the LLMs to get anything consistent.</p>

<p>What I was doing, every single time, was asking the model to invent the style and the pose simultaneously, in the same prompt, with no shared context between sessions. Of course the army drifted. I was running fifty independent experiments and then complaining that they did not match.</p>

<p>So each model I made, I added another constraint to the prompt. Mistake on a model, add a constraint. Mistake on the next model, add a constraint. No thin protrusions. Round integrated base. Chibi proportions. Single solid silhouette. The prompt got longer. The models got slightly more consistent. I was slowly, painfully, by hand, reverse-engineering a brief and not realising that was what I was doing.</p>

<p>Somewhere around the lieutenant or the sniper, the penny dropped.</p>

<p><img src="/assets/army-of-prompts/german-lieutenant.jpg" alt="german lieutenant" /></p>

<h2 id="separate-the-style-from-the-pose">Separate the style from the pose</h2>

<p>The mistake was treating each prompt as “make me a model.” What I actually wanted was to separate two things that I had been asking the model to do at the same time, and to do them in a specific order. Style is the thing that has to be consistent across the whole army. Pose is the thing that needs to vary per unit. Conflating those is how you get visual chaos.</p>

<p>But the order of operations matters even more than that, and this is the bit that took me the longest to figure out.</p>

<p>The Soviet workflow goes like this.</p>

<p>Start a fresh chat. Drop in the prompt. Not an image. Just the prompt. The whole brief. The unit description, all the style constraints, the silhouette rules, the front-and-back-must-match rules, and at the bottom: <em>suggest three variations for poses only</em>.</p>

<p>What that does is get ChatGPT to <em>think</em>. The chat reads through the brief and writes back three pose ideas in words. Gunner prone with the rifle braced, loader pointing toward the target. Or both kneeling behind cover. Or one standing scanning, the other reloading. Whatever the unit calls for.</p>

<p>This step is not about getting poses I want to use. It is about seeding the chat’s context with the right frame of mind. By the time it has written out three pose options, it has already worked through the brief on its own terms. It is now thinking inside the constraints I gave it, instead of about to be asked to obey them.</p>

<p><em>Now</em> I drop in the seed image. The commissar. And I say: generate those poses, in this style, front and back.</p>

<p>That is the move. The seed image arrives after the thinking has already happened, not before. The chat is not asked to invent a style and a pose at the same time. It does the pose work first, on its own, in words. Then the style gets bolted on as a visual reference to a frame of mind that already exists.</p>

<p>For the Soviets I used the commissar as the seed. The kind of cartoon menace whose whole vibe is “shoot anyone who tries to run away.” That image carried the entire visual identity of the army. Every other model would be made to match it.</p>

<p><img src="/assets/army-of-prompts/commisar.jpg" alt="commissar" /></p>

<p>Here is the prompt I used, every time, varying only the unit description. I will paste it in full because the prompt itself is the artefact. I spent half a German army learning how to write it.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>goal: designing soviet ww2 soldiers keeping in style with the images above from 2 perspectives (front and back)

give me 3 variations of poses

* **AT rifle team (2 models):** gunner prone firing PTRD-41 (exaggerate the absurd length of the rifle — historically over 2m, push it visually), loader kneeling alongside pointing at imagined target (the German halftrack). Mixed gritty dress, SSh-40 helmets, prominent extra ammo pouches/satchels for the big AT rounds.

Render the models in the reference style including:

* "Chibi proportions, large head roughly 1/3 body height, oversized hands and weapon"
* "Chunky oversized weapon, simplified details, no thin protrusions"
* "Integrated round base, feet merged to base"
* "Single solid silhouette, no floating straps or separated gear"
* weapons should be representative and oversized so that they survive 3d printing at small scale (2 cm)
* "Back view must be the exact same pose rotated 180 degrees"
* "Weapon position must match exactly between front and back"
* "No reinterpretation of pose"
* "Single sculpt shown from two angles, not two separate sculpts"
* "Silhouette must align when mirrored"

Suggest 3 variations for poses only 
</code></pre></div></div>

<p>Notice what is going on here. I am not asking for a model. I am not even asking for an image. I am asking for <em>poses, in words</em>. The style brief is loaded into the chat’s context, but what comes back at this stage is text. Three written pose options. The image generation is the next turn, after I have read those options and decided which one I want, and after I have shown it the seed image to lock the visual style.</p>

<p>I am also, very explicitly, telling it that front and back are the same sculpt seen from two angles. Not two separate models. The same model, mirrored. This matters enormously when those images go into Meshy, because Meshy will happily interpret two different poses as two different sculpts and produce something that looks like the soldier sneezed mid-print.</p>

<p>Then the seed image goes in, the style locks, and the image generation begins. And here the “suggest three variations” framing pays off again. Not one. Three. Because once the chat is preloaded with the right context, generating variations is essentially free, and what you actually want is a buffet of options. Generate three poses. Look at them. Generate three more. Look again. Generate three more. Within ten minutes I had a wall of images for any given unit and I could just go yeah, that one, that one, not that one, that one. Pick the pose that looked most like what was in my head and move on.</p>

<div class="nco-carousel" style="position: relative; width: 100%; max-width: 800px; margin: 1.5rem auto; text-align: center;">
  <div class="nco-carousel-track" style="display: grid; border-radius: 4px; overflow: hidden;">
  
  
    <img src="/assets/army-of-prompts/ncos/nco1.jpg" alt="NCO variation 1" data-idx="0" style="grid-area: 1 / 1; width: 100%; height: auto; opacity: 1; transition: opacity 0.25s;" />
  
    <img src="/assets/army-of-prompts/ncos/nco2.jpg" alt="NCO variation 2" data-idx="1" style="grid-area: 1 / 1; width: 100%; height: auto; opacity: 0; transition: opacity 0.25s;" />
  
    <img src="/assets/army-of-prompts/ncos/nco3.jpg" alt="NCO variation 3" data-idx="2" style="grid-area: 1 / 1; width: 100%; height: auto; opacity: 0; transition: opacity 0.25s;" />
  
  </div>
  <button type="button" class="nco-prev" aria-label="Previous" style="position: absolute; top: 50%; left: 0.5rem; transform: translateY(-50%); background: rgba(0,0,0,0.55); color: #fff; border: 0; border-radius: 50%; width: 2.25rem; height: 2.25rem; font-size: 1.25rem; cursor: pointer;">‹</button>
  <button type="button" class="nco-next" aria-label="Next" style="position: absolute; top: 50%; right: 0.5rem; transform: translateY(-50%); background: rgba(0,0,0,0.55); color: #fff; border: 0; border-radius: 50%; width: 2.25rem; height: 2.25rem; font-size: 1.25rem; cursor: pointer;">›</button>
  <div class="nco-counter" style="margin-top: 0.5rem; font-size: 0.85rem; color: #666;"><span class="nco-current">1</span> / 3</div>
</div>
<script>
(function () {
  var root = document.currentScript.previousElementSibling;
  var imgs = root.querySelectorAll('.nco-carousel-track img');
  var counter = root.querySelector('.nco-current');
  var i = 0;
  function show(n) {
    i = (n + imgs.length) % imgs.length;
    imgs.forEach(function (img, idx) { img.style.opacity = idx === i ? '1' : '0'; });
    counter.textContent = i + 1;
  }
  root.querySelector('.nco-prev').addEventListener('click', function () { show(i - 1); });
  root.querySelector('.nco-next').addEventListener('click', function () { show(i + 1); });
})();
</script>

<p>Once I picked a pose, I screenshotted the front and back, dropped both into Meshy, and let it generate. With both views provided, Meshy had much less room to invent. It was joining up two views I had already approved, rather than hallucinating the missing half of the model.</p>

<p><img src="/assets/army-of-prompts/meshy.jpg" alt="meshy models" /></p>

<p>After that it was mechanical. Export STL, not 3MF, because 3MF kept giving me non-manifold errors no matter what Meshy claimed about the export. Import to Bambu Studio. Scale to 2 centimetres. Simplify the mesh by 96 percent, which is an insane level of deformation that nonetheless came out at perfectly decent resolution given the size I was printing at. Slice. Print. Move on.</p>

<p><img src="/assets/army-of-prompts/riflemen.jpeg" alt="riflemen" /></p>

<h2 id="print-plates-as-units-not-as-inventory">Print plates as units, not as inventory</h2>

<p>One small thing that changed between the German and Soviet builds. With the Germans I had treated my STL library as a parts bin. When I wanted to print a unit, I would go pick the right models, drag them onto a plate, configure supports, slice. Every print was a setup task.</p>

<p>With the Soviets I built each plate <em>as the unit it represented</em>. The veteran plate is the veteran unit. The conscripts plate is the conscripts. The AT rifle team is its own plate. When I want to print a unit, I open the file and click print. No setup. No selection. No accidentally forgetting the squad leader.</p>

<p>This is a tiny change and it made a disproportionate difference to how often I actually printed things. Friction matters. I have written this exact lesson down about five times in the context of software and apparently I needed to learn it again with plastic.</p>

<h2 id="the-discipline-transfers">The discipline transfers</h2>

<p>What I just described, concept, iteration, refinement, codification into a reusable artefact, production pipeline, quality controls, delivery, is not a 3D printing process. It is a software development process. It is the same process I have been writing about for months in the context of agentic coding. Product thinking first. Iterate to find the brief. Codify the brief into something reusable. Treat each output as one of many. Build the production pipeline so the next thing is trivial.</p>

<p>The German army was vibe coding without discipline. Lots of energy, lots of output, no consistency, mounting technical debt with every model. The Soviet army was the same domain, the same tools, the same person, with a <em>process</em> in between. The result is not a small improvement. It is a different category of thing. Adding a new unique sculpt to the Soviet army from this point forward is virtually trivial. It is stupid how simple it is. It should not be this simple.</p>

<p>I have spent a year writing about how the harness matters more than the model, how tests and constraints are really a discipline of conscious decisions about every line, how multi-harness workflows surface regressions you would otherwise eat. I thought I was writing about software. It turns out I was writing about a way of working that translates, more or less unchanged, the moment the output head changes from “code” to “plastic.”</p>

<p>What broke my brain about the first printed soldier was that the loop existed at all. The thing that is breaking my brain about this post is that the <em>discipline</em> transfers. Whatever I figure out about working well with these tools in software is, apparently, immediately applicable to a domain I have no formal training in.</p>

<p>I sat down to write a blog post about printing little Soviet soldiers. I ended up writing one about how surprised I am that what I thought I knew about working with these tools in code worked, unchanged, in plastic.</p>

<p>I do not want to make that bigger than it is. One person, one weekend, one transfer. But it is the kind of small surprise that makes me look sideways at every other thing I think I know about my own discipline. Some of it is presumably about software. Some of it, evidently, is not.</p>

<h2 id="appendix-things-that-will-save-you-time-if-you-are-doing-this">Appendix: things that will save you time if you are doing this</h2>

<p>A few sharp edges I hit, written down so you do not have to.</p>

<p><strong>Export STL from Meshy, not 3MF.</strong> Meshy’s 3MF export gave me non-manifold errors with depressing reliability, even when the tool insisted there were none. STL behaved.</p>

<p><strong>Simplify aggressively.</strong> A Meshy model can come out at over a million vertices. At 2 centimetre print scale, you genuinely do not need that. I was simplifying down by 96 percent in Bambu Studio and the prints still came out crisp.</p>

<p><strong>Custom support settings for tiny prints.</strong> The defaults will fuse supports to the model and make removal a nightmare. The settings I landed on for 0.08mm layers:</p>

<table>
  <thead>
    <tr>
      <th>Setting</th>
      <th>Default</th>
      <th>What I use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Top Z Distance</td>
      <td>0.08–0.1mm</td>
      <td>0.16–0.20mm</td>
    </tr>
    <tr>
      <td>Top Interface Layers</td>
      <td>2</td>
      <td>3</td>
    </tr>
    <tr>
      <td>Interface Pattern</td>
      <td>Rectilinear</td>
      <td>Rectilinear Interlaced</td>
    </tr>
    <tr>
      <td>XY Distance</td>
      <td>0.35mm</td>
      <td>0.5mm</td>
    </tr>
  </tbody>
</table>

<p>Increasing the Top Z Distance is the single biggest win. At fine layer heights, a one-layer gap is not enough to stop the support fusing to the print. Doubling or tripling it gives the filament enough room to drop onto the support rather than weld to it. Tree Slim or Tree Organic for style. They use less material and break away cleanly.</p>

<p><strong>Generate front and back from the same prompt.</strong> Do not let Meshy interpret the back view. Do not let it interpret anything. Give it both views explicitly. Tell ChatGPT, in the prompt, that the back view is the exact same pose rotated 180 degrees. Belt and braces. You will get what you want far more often.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="aios" /><category term="claudecode" /><category term="opencode" /><summary type="html"><![CDATA[On discovering that good process survives the jump from code to plastic.]]></summary></entry><entry><title type="html">Show Your Work</title><link href="https://www.petervanonselen.com/2026/05/06/show-your-work/" rel="alternate" type="text/html" title="Show Your Work" /><published>2026-05-06T08:00:00+00:00</published><updated>2026-05-06T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/05/06/show-your-work</id><content type="html" xml:base="https://www.petervanonselen.com/2026/05/06/show-your-work/"><![CDATA[<p><em>On discovering that “show your work” is not the same thing as “do the work well.”</em></p>

<hr />

<p><img src="/assets/show-your-work/hero.png" alt="hero image" /></p>

<p>When I first started using Claude Code last summer, it felt like a chatty junior engineer who couldn’t wait to tell you what it was thinking. It kept you in the loop. It explained itself. It told you what it was about to try, why it thought that might work, and then narrated its way through whether it did. There was charm in it. You felt like you were pairing.</p>

<p><img src="/assets/show-your-work/claude-silence.png" alt="Claudes silence" /></p>

<p>Now Claude Code sits there like a Zen Buddhist monk, slowly mulling the problem in silence, and then suddenly exclaims “TADA!” and hands you a diff. This is not the chatty pair I remember. All the in-depth thinking, the running commentary, the visible reasoning that gave it such charm has quietly disappeared, and what’s left is this churning quiet system that is all work and no play.</p>

<p>So I have been leaning more and more on opencode. It talks. It thinks in a personable way. It structures its thoughts so I can follow along. And more importantly, it doesn’t hide its working. It felt like maths in school: show your work. Opencode shows its work. Claude Code doesn’t. And it has been astonishing to me how much I valued being able to read the thinking along the way.</p>

<p>That is where this post would have ended a few weeks ago. A grumpy aside about how Claude Code lost its voice. Opencode won, etc, etc. Move on.</p>

<p>Except the question wormed its way in. <em>Am I right?</em> I have a strong opinion about which harness you should use, and the opinion is built almost entirely on vibes. On who I enjoy talking to. On whether the chat feels good. None of that tells me anything about which harness actually produces better code. And once that question is in your head it doesn’t leave.</p>

<p>I thought I was building a small harness comparison. In hindsight I was building something stranger: a tiny automated judgement machine for code, of the sort I had been hearing people gesture at without quite understanding what they meant.</p>

<h2 id="a-small-experiment">A small experiment</h2>

<p>So I tried to be a bit Baconian about it. Same prompt, same starting state, run it through every harness and model combination I could get my hands on, multiple times, and grade the output against tests the agent never gets to see.</p>

<p>The setup was a fake Library API. A small but not trivial OpenAPI spec covering books, loans, fines, and members. Cursor pagination. Polymorphic loan responses where active loans look different from returned ones. An async payment polling flow with a terminal state. Structured API errors that should be preserved through the client. The agent’s job was to build a typed TypeScript client. No code generation, no runtime dependencies, just read the spec and write the thing.</p>

<p>The catch was a hidden test suite. The agent could write its own tests, run its own validation, do whatever it wanted to convince itself the implementation worked. But the grading happened against a separate Vitest suite the agent never saw. That suite poked at all the bits I expected harnesses to get wrong: did the cursor pagination actually iterate, did the async payment poll reach a terminal state, did the polymorphic loan response preserve both shapes, did API errors surface usefully or get flattened into “Error: request failed”. The agent could not optimise to it because it could not see it.</p>

<p>Six combinations. Five runs each. Thirty implementations of a Library API client. The rig is on <a href="https://github.com/vanonselenp/harness-bench">GitHub</a> if you want to poke at it.</p>

<p>I did not go into this neutrally. I had a favourite. I expected opencode-opus to win, claude-code to look quietly competent but a bit lifeless, and the GPT-backed runs to come in mid-pack. I thought I knew the shape of the answer before I started.</p>

<p>While I was setting it up, something else started to nag at me. I had been hearing the term “dark factory” floating around in the agentic coding conversation and not really understanding what people meant. A factory that runs without lights because there are no humans in it. Applied to coding, it suggested some end state where you specify what you want, an agent produces it, and something else judges whether it is correct, all without you in the loop. I had nodded along when people brought it up and quietly had no idea how you would actually build one. But the rig I was assembling was starting to look uncomfortably like a small piece of that picture, and I tried not to think about it too hard while I was still building the thing.</p>

<h2 id="the-results">The results</h2>

<p>I will spare you the full grid. Each run was scored out of 20 hidden tests, and each harness/model pair ran five times, so the maximum score per row is 100. The headline numbers:</p>

<table>
  <thead>
    <tr>
      <th>Harness</th>
      <th style="text-align: right">Hidden tests</th>
      <th style="text-align: right">Perfect runs</th>
      <th style="text-align: right">Median diff</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>claude-code</td>
      <td style="text-align: right">98/100</td>
      <td style="text-align: right">4/5</td>
      <td style="text-align: right">1157</td>
    </tr>
    <tr>
      <td>opencode-opus</td>
      <td style="text-align: right">97/100</td>
      <td style="text-align: right">3/5</td>
      <td style="text-align: right">628</td>
    </tr>
    <tr>
      <td>pi-gpt</td>
      <td style="text-align: right">92/100</td>
      <td style="text-align: right">1/5</td>
      <td style="text-align: right">1444</td>
    </tr>
    <tr>
      <td>pi-opus</td>
      <td style="text-align: right">90/100</td>
      <td style="text-align: right">1/5</td>
      <td style="text-align: right">1401</td>
    </tr>
    <tr>
      <td>codex</td>
      <td style="text-align: right">85/100</td>
      <td style="text-align: right">2/5</td>
      <td style="text-align: right">863</td>
    </tr>
    <tr>
      <td>opencode-gpt</td>
      <td style="text-align: right">85/100</td>
      <td style="text-align: right">2/5</td>
      <td style="text-align: right">732</td>
    </tr>
  </tbody>
</table>

<p><em>Median diff is measured in lines changed per run.</em></p>

<p>Claude Code won on correctness. Four perfect runs out of five. The harness I had been quietly resenting for going silent on me produced the most reliable code in the experiment. I sat with that for a bit. The chatty junior I missed had grown up into the engineer who just gets it done and hands you the result, and apparently the result is good.</p>

<p>Opencode-opus came in essentially tied on correctness, one point behind. But look at the median diff. 628 lines vs 1157. Same task, same spec, near identical scores, and opencode-opus did it in a little over half the code. If you measure tests passed per hundred lines of diff, opencode-opus is comfortably the best of the lot at around three. Claude Code is a touch under two. Pi-opus is at one and change. It is a crude metric, obviously. Fewer lines are not inherently better, and there are plenty of ways to cheat at it. But when two runs are almost tied on correctness and one gets there with half the diff, I pay attention. Claude Code is most reliable. Opencode-opus is most efficient. I am genuinely not sure which I value more in a real engineering context.</p>

<p>The other observation that I keep turning over is the pi.dev runs. Largest diffs in the experiment, mid-pack on correctness. More generated code did not buy reliability. It is tempting to read a wall of plausible-looking output as evidence of diligence. The data here says that intuition is wrong. Verbose was not safer. Verbose was just verbose.</p>

<p>A caveat there, though, and an important one. The pi.dev runs were stock pi out of the box. No customisation, no extra tools, no extension of its capabilities. And the whole point of pi is that you customise it. That is the entire pitch. So what I actually measured was vanilla pi, with a deliberately limited toolkit, on a task that probably wanted a richer one. Seen that way, the fact that pi-gpt landed at 92 with no help from me is genuinely interesting. There is more to do here, and a properly outfitted pi might tell a different story. That is its own post.</p>

<p>The same-model-different-harness comparison is where it gets weirder. Codex and opencode-gpt are both GPT-5.5 doing the same task, and they tied on aggregate score, but opencode-gpt did it in noticeably less code. Same brain, different harness, different shape of output. Then flip it: opencode-gpt vs opencode-opus is the same harness with different models, and the model swap moved the score from 85 to 97. So harness matters. Model matters. They are not interchangeable variables, and which one matters more depends on which one you change.</p>

<h2 id="what-this-rig-had-become">What this rig had become</h2>

<p>Once the runs were done and I was staring at the spreadsheet, the thing I had been trying not to think about earlier became impossible to ignore.</p>

<p>I had a detailed work order in <code class="language-plaintext highlighter-rouge">prompt.md</code> and <code class="language-plaintext highlighter-rouge">AGENTS.md</code> and a formal OpenAPI spec. I had a clean starting workspace that got reset between runs. I had a constrained implementation environment with locked Node and TypeScript versions. I had a hidden acceptance test suite the agent could not see or game. I had a mock service that behaved enough like a real one to be tested against. I had a runner that could launch any of the harnesses under comparable conditions, and a grader that built the output, ran the hidden tests, captured the diff size, and recorded the results into a table.</p>

<p>That is most of a dark factory. Spec in, code out, automated judgement in the middle, results captured for analysis, no human required during a run. The piece that is missing is the feedback loop. Right now, when a run scores 15 out of 20, that is the end of the story. It gets logged and we move on. A dark factory v0.1 would read those failures, decide whether to retry, mutate the prompt or the constraints, launch another attempt, and keep going until the score crossed some threshold or the budget ran out.</p>

<p>I do not have that yet. But I can see how to build it from here, which is something I genuinely could not see a month ago. I had been hearing the term and nodding politely. Now I had accidentally built most of one because I was trying to settle a vibes-based argument with myself about which harness I liked best.</p>

<p>And the thing that makes me uncomfortable about that is what it implies about “show your work”. I had been treating the visible reasoning as a proxy for trust. Watching opencode talk through the problem made me feel like it knew what it was doing. Watching Claude Code go quiet made me feel like it didn’t. The hidden tests did not care about either of those feelings. They cared about whether the cursor pagination iterated, whether the polymorphic loan response preserved both shapes, whether the async payment poll reached a terminal state. Visible reasoning helped me supervise. Hidden tests measured whether the work was actually done. Those turn out not to be the same thing, and inside a dark factory only one of them survives, because there is no human there to be reassured by the other.</p>

<h2 id="what-i-am-left-with">What I am left with</h2>

<p>I expected this experiment to vindicate opencode. It did not. If I cared only about pass rate I would use Claude Code. If I cared about correctness density I would use opencode-opus. If I cared about reading the agent’s reasoning while it works, I would still use opencode, and I will. Aesthetics matter. I spend hours in this tool. I want to enjoy being there.</p>

<p>But I am going to stop pretending that preference is a quality argument. It is a supervision argument, which is a different thing, and one that gets thinner the further you move toward letting the rig run on its own.</p>

<p>The other thing I am left with is the rig itself. I started building it to answer a small question and ended up with a thing that has the shape of something much bigger. That is the part I did not see coming. The benchmark was supposed to be the point. It turns out the benchmark might just be the prototype.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="aios" /><category term="claudecode" /><category term="opencode" /><summary type="html"><![CDATA[On discovering that “show your work” is not the same thing as “do the work well.”]]></summary></entry><entry><title type="html">Your Scientists Were So Preoccupied</title><link href="https://www.petervanonselen.com/2026/05/04/your-scientists-were-preoccupied/" rel="alternate" type="text/html" title="Your Scientists Were So Preoccupied" /><published>2026-05-04T08:00:00+00:00</published><updated>2026-05-04T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/05/04/your-scientists-were-preoccupied</id><content type="html" xml:base="https://www.petervanonselen.com/2026/05/04/your-scientists-were-preoccupied/"><![CDATA[<p><em>That they forgot to ask whether SSHing into an AI coding agent from a phone was a good idea.</em></p>

<hr />

<p><img src="/assets/phone-doom.png" alt="phone distraction device of doom" /></p>

<p>This is a thing you should not do. I want to say that up front, before any of the rest of it, because the rest of it is a reasonably well-considered guide to doing the thing, and I do not want anyone reading the guide bit and thinking that I am endorsing what comes after it.</p>

<p>The phone is already a distraction device of doom. I have spent a non-trivial amount of effort over the last couple of years trying to make mine less of one, with limited success. What I am about to describe takes that distraction device of doom and turns it into a distraction device of doom that can also write and ship code. This is, on every reasonable axis I can think of, a worse situation than the one I started in.</p>

<p>But.</p>

<p>I was on a train recently. One of those medium-length train journeys that everyone in the UK eventually finds themselves on. About an hour and a half. Two or three changes. Never quite enough uninterrupted sit-down time for pulling out a laptop to make sense. You can read a book. You can stare blankly at your phone. What you cannot do is meaningfully open an IDE and ship a feature. And yet there I was, with a project I wanted to be working on, several connections to make, and a phone in my pocket. So I built the thing. Here is how.</p>

<h2 id="the-bits-you-need">The bits you need</h2>

<p>You need <a href="https://tailscale.com/">Tailscale</a>. You need <a href="https://mosh.org/">mosh</a>. You need <a href="https://termius.com/">Termius</a>. You need <a href="https://github.com/connorads/remobi">remobi</a>. You need tmux. And if you are doing this from a Mac that lives plugged in with its lid open, you need to know that <code class="language-plaintext highlighter-rouge">caffeinate -is</code> exists. You also need enough self-control not to use any of this irresponsibly, which is where the plan begins to fall apart.</p>

<h2 id="tailscale">Tailscale</h2>

<p>Tailscale is a way of pretending that two devices on entirely different networks are actually on the same one. You install it on your laptop, you install it on your phone, and from that point on the two of them have stable IPs that work no matter which café WiFi or train 4G you happen to be straddling at the time. This is the foundation. Without this, none of the rest works, because your phone has no idea where your laptop is and your laptop has no interest in being found by random strangers on the internet, both of which are correct positions for them to hold.</p>

<h2 id="mosh">mosh</h2>

<p>SSH is a beautiful protocol that falls apart the moment your connection blinks. Trains go through tunnels. 4G drops to 3G drops to nothing and back again. SSH does not enjoy this. Mosh does. Mosh is a shell client and server that survives the kind of network conditions you get when you are physically moving through countryside at speed, which is exactly the situation we are designing for here.</p>

<h2 id="tmux">tmux</h2>

<p>You want a terminal session that just keeps running on your machine whether you are connected to it or not. tmux is the obvious answer. Start a session, leave it there, reattach to it later from whatever client you happen to be using at the time. This is the bit that makes the whole thing feel less like a fragile remote connection and more like walking back to a desk you left an hour ago.</p>

<h2 id="termius">Termius</h2>

<p>Termius is a genuinely lovely SSH client. It runs on your phone, it knows about your Tailscale IPs, and it gives you a terminal that you can just type into. You hit your laptop over mosh, attach to your tmux session, and away you go. If your agent of choice is Claude Code or Codex CLI or OpenCode or whatever flavour of the week you are running, this is enough to be productive. You point it at the thing, you tell it to go, and you watch it work.</p>

<h2 id="remobi">remobi</h2>

<p>remobi is the bit that turned this from “technically possible” into “actually quite nice.” It was written by <a href="https://github.com/connorads">Connor Adams</a>, who is one of those quietly talented engineers who keeps producing useful things while the rest of us are still talking about producing things. What it does is run a little server on your machine that exposes your tmux session over HTTP, which means you can wrap it up as a desktop app on your phone and just tap to be back where you were.</p>

<p>The reason this matters is that the UI is better than typing into a phone-shaped SSH client. You get native scrolling. You get sensible zoom. You get a layout that does not require you to remember which gesture corresponds to which control sequence. It feels less like fighting your phone and more like using it.</p>

<h2 id="caffeinate">caffeinate</h2>

<p>If your laptop is the kind of laptop that lives plugged in with the lid open, you have two problems. First, you need to enable Remote Login, which is the kind of setting you turn on once and then forget exists until you need it again. Second, you need <code class="language-plaintext highlighter-rouge">caffeinate -is</code>, which is a macOS command I did not know about until very recently and which apparently ships with the operating system. It tells the system to stop being clever about going to sleep when the display turns off. Run it, leave it running, and your machine stays awake long enough to actually be useful as a remote target.</p>

<h2 id="so-now-what">So now what</h2>

<p><img src="/assets/remobi.png" alt="A perfectly normal and healthy thing to be doing from a phone." /></p>

<p>Now you have the ability to write code from your phone, anywhere, regardless of whether you are sitting at a desk or wedged into a train seat with your bag on your lap. You can kick off an agent, watch it work, course-correct it, and have something meaningfully shipped by the time you arrive at wherever you were going. On the train I was on, this would have neatly solved the problem of being bored and wanting to work on something I could not work on.</p>

<p>I should mention, in fairness, that you could probably do most of this with Claude Code’s mobile app and save yourself the entire setup. That is true. The reason I did not is that I am stubbornly committed to not being locked into any one AI toolset, which means I want the option to point this same setup at Codex and OpenCode and Claude Code and whatever else I happen to be running that week. So I have reinvented a wheel that already exists, in order to have a wheel that I own.</p>

<h2 id="the-bookend">The bookend</h2>

<p>Here is the thing though. I am not actually sure I have done myself any favours.</p>

<p>The phone, as previously established, is the world’s ultimate distraction device. I am, very slowly and with mixed results, trying to make mine less of one. And what I have done here is take that device, the one that is already eating more of my attention than I would like, and given it a brand new way to consume me. Now it is not just a thing I pick up to check the time and put down forty minutes later wondering where the time went. Now it is also a thing I can pick up to “just quickly check on the agent” and put down forty minutes later wondering where the time went, except this time I have shipped half a feature I had not actually decided I wanted to ship yet.</p>

<p>I was so busy thinking about whether I could that I did not stop to ask whether I should. Friends, I should not have. Your mileage may vary.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="aios" /><category term="claudecode" /><category term="opencode" /><summary type="html"><![CDATA[That they forgot to ask whether SSHing into an AI coding agent from a phone was a good idea.]]></summary></entry><entry><title type="html">I Built This In A Prompt Window! With A Box Of Filament!</title><link href="https://www.petervanonselen.com/2026/04/22/vibe-coding-reality/" rel="alternate" type="text/html" title="I Built This In A Prompt Window! With A Box Of Filament!" /><published>2026-04-22T08:00:00+00:00</published><updated>2026-04-22T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/04/22/vibe-coding-reality</id><content type="html" xml:base="https://www.petervanonselen.com/2026/04/22/vibe-coding-reality/"><![CDATA[<p><em>I Vibe Coded A Model Into My House</em></p>

<hr />

<p>There is a 3D printed World War II German infantryman sitting on my desk. He is about the size of my thumb, slightly chibi in the proportions, with a helmet a touch too large for his head. He looks, frankly, adorable. <em>He is also not a copy of anything</em>. Nobody designed him. Nobody sculpted him. Nobody even sketched him. I typed some words at a screen, pressed a button on a different screen, and twenty minutes later he was sitting on my desk. On the left pure vibes, on the right reality.</p>

<p><img src="/assets/reality/hero.jpg" alt="hero" /></p>

<p>I have been writing this blog for a while now about adventures in applying AI to dev related problems. Writing code. Building software. Making things. It has taken me through a Magic jumpstart cube generated with code, a board game prototype, a re-implementation of that prototype in Godot, a stealth tactics turn based game also in Godot, a news digest SaaS that is still somewhere in the middle of becoming itself, and a frankly embarrassing pile of bash scripts accumulated from deep dives into my coding harnesses. Along the way I have taken what I have learned and applied it at work. It has been a wonderful, bizarre road.</p>

<p>This post is about atoms instead of bits, which is a first for the blog. I promise it rhymes.</p>

<h2 id="the-printer">The printer</h2>

<p>Somebody gave me a 3D printer. An A1 Mini, which is one of the cheaper ones but turns out to be remarkably capable for what it is. And as one does when one acquires a 3D printer, I immediately spent a week not printing anything interesting. I built shelves for my office to create space. I printed spool holders for the spools. I printed the tools you need to use the 3D printer. This seems to be the compulsory onboarding ritual when you get a 3D printer, which is a bit like spending your first week with a new laptop installing a package manager so you can install the package managers that let you install things. Fine. Tradition.</p>

<p>Then I set myself an actual goal.</p>

<h2 id="the-weird-hobby-compulsion">The weird hobby compulsion</h2>

<p>I should explain that I have a tendency to go on strange game-making tangents. I have built a travel sized version of a board game from scratch with custom cards and components, purely because it was fun to tinker with. I have made print and play versions of expansions for games I already own. I made my own version of Santorini using tiles and spray paint, like some sort of deranged hobbyist. I paint, I draw, I mess about. The point is that “I wonder if I could just make this” is my default failure mode when I encounter any game that costs too much or takes up too much space.</p>

<p>Bolt Action has been living rent free in the back of my head for two or three years now. It is a tabletop wargame. I would like to play it. But I do not want to spend the money on the models, I do not want to find the table space, I do not know enough people in my area who want to play it with me, and painting a hundred models is a lot of effort in a way that is genuinely hard to appreciate until you have sat there and done it.</p>

<p>The idea that has been sitting in the back of my brain for a few years is this: make a scale down version. Instead of moving in inches, move in centimetres. Same rules, same ratios, everything else the same, just smaller. Smaller table. Smaller models. Smaller paint commitment.</p>

<p>Which means you need smaller models. Which has always been the problem.</p>

<h2 id="the-accidental-pipeline">The accidental pipeline</h2>

<p>I saw a <a href="https://talesfromfarpoint.blogspot.com/2026/03/junker-update-and-tiny-troops-how-to.html?m=1">blog post a while back by a guy making models out of Fimo clay and EVA foam and little bits of wood</a>. They were cute and chibi and slightly weird and I really liked them. So naturally I tried to make one myself. What I ended up with functionally looked okay and matched the vibe but was basically 28mm scale, which is normal Bolt Action size, which defeats the entire point.</p>

<p>On a whim, I took the photo the original guy had taken of his model and fed it into Gemini Pro. “This style. World War II Germans.”</p>

<p><img src="/assets/reality/all.jpg" alt="all" /></p>

<p>Gemini came back with something that looked astonishingly good. Cute, chibi, right proportions, right register. Something that immediately felt like what I had been trying to describe for years without quite having the vocabulary for it. I then started giving it more structured prompts. Give me a commanding officer. Give me an NCO. Give me a machine gunner.</p>

<p><img src="/assets/reality/meshy.jpg" alt="Meshy produced a 3D model" /></p>

<p>I took one of these entirely hallucinated images, cropped it, and fed it into Meshy, which is a generative tool that takes an image and produces a 3D model.</p>

<p>I then, mostly out of morbid curiosity, copied the file over to the printer and clicked print.</p>

<p><img src="/assets/reality/first.jpg" alt="hot off the print bead" /></p>

<h2 id="this-confounds-me">This confounds me</h2>

<p>Twenty minutes later, there was a physical object on my desk. A German infantryman. About the size of my thumb. Cute and chibi, helmet slightly too large. Precisely the thing I had been describing to Gemini about half an hour earlier.</p>

<p>I want to be clear about what happened here because I think I am still processing it.</p>

<p>I described a thing in words. Another thing dreamed up a picture of that thing, a picture that had never previously existed. A third thing hallucinated a 3D shape from that picture, a shape that had also never previously existed. A fourth thing turned that shape into an object I can hold in my hand. No human sculpted it. No human modelled it. No human even sketched it. The infantryman on my desk has no reference in the world. He is not a copy of anything. He is purely the output of a <em>pipeline of vibes</em>.</p>

<p><em>I vibe coded a model into my house</em>.</p>

<p>I have spent a year now playing around with generative AI. I have been, at times, out on what I thought was the frontier of what it is doing. I should have seen this coming. At some level I had seen this coming, in the abstract, “yes of course generative AI plus 3D printing, that is obviously a thing” way. But there is a chasm between knowing a thing is possible and holding the output of that thing in your hand twenty minutes after describing it out loud.</p>

<p>This is the same loop I have been running on software for a year. Describe a thing, get a thing, iterate, ship. The loop works on atoms now. It has probably been working on atoms for a while and I simply had not wired up the last step of the pipeline in my own life until somebody gave me a printer.</p>

<p>Which makes me wonder what else is already sitting there, loop closed, waiting for me to notice.</p>

<h2 id="what-im-doing-with-it">What I’m doing with it</h2>

<p>I am now in the middle of printing a 500 point German army and a 500 point Soviet army. The whole thing will probably fit in a box slightly bigger than a paperback. A deck of cards each for the rules and unit references. Total cost of the models, given that I already had the printer and the filament: functionally nothing. If a friend wanted to play, I could just print them a second army and not be fussed about it. There is some manual work around trimming supports and cleaning up sprues, but it is not the kind of work that scales with ambition. It scales with how many figures you feel like cleaning up on a given evening.</p>

<p>Something I have been idly wanting for two or three years is just there now, in a box, because the pipeline finally closed.</p>

<p><img src="/assets/reality/current.jpg" alt="current printed" /></p>

<h2 id="what-i-cant-get-out-of-my-mind">What I can’t get out of my mind</h2>

<p>Here is the thing that has been rattling around my head since the infantryman showed up.</p>

<p>We are, collectively, still arguing about whether vibe coded software counts as real engineering. That argument is live. It is on my timeline every week. It is in the comments of every post I write. People who build things for a living are genuinely unsure whether the loop of “describe a thing, get a thing, ship it” is a legitimate way to make software, and reasonable people disagree about that, and the discourse is maybe a year behind the tools and possibly more.</p>

<p>While we have been having that argument, the same loop has quietly grown another output head. It makes physical objects now. Not in some research lab, not in some well funded startup I would need to buy into. In my house, on a desk, using tools that anyone can download or buy, for a material cost measured in pennies per figure.</p>

<p>I found this out by accident. Someone gave me a printer. I had an itch I had been scratching at for years, and the pipeline closed on its own while I was not really paying attention. That is what is unsettling me. Not that it works. I knew it would work. It is that I walked into this corner of it entirely by accident, with no plan, and the corner was just sitting there waiting for anyone who happened to wander in.</p>

<p>I do not think we are ready for the software version of this. I am much less sure about anything else. Because if a ten minute detour into a completely different hobby is enough to produce an object with no human author, what else is already sitting there that I have not stumbled into? What other loops have quietly closed while I was looking at my terminal? I thought I had been playing on the frontier for a year. It turns out I have been playing in one room of a house whose floor plan I do not have.</p>

<p>I do not have a tidy ending for this. I have an infantryman on my desk, and a suspicion that I have been looking at a very small corner of something, and that almost everyone I know has been looking at the same small corner, and that the rest of the house is already built.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="aios" /><category term="claudecode" /><category term="softwarecraftsmanship" /><summary type="html"><![CDATA[I Vibe Coded A Model Into My House]]></summary></entry><entry><title type="html">Conscious Coverage</title><link href="https://www.petervanonselen.com/2026/04/16/concious-coverage/" rel="alternate" type="text/html" title="Conscious Coverage" /><published>2026-04-16T08:00:00+00:00</published><updated>2026-04-16T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/04/16/concious-coverage</id><content type="html" xml:base="https://www.petervanonselen.com/2026/04/16/concious-coverage/"><![CDATA[<p><em>We don’t talk about Code coverage, no no no, we don’t talk about coverage…</em></p>

<hr />

<p><img src="/assets/coverage.png" alt="code coverage matters" /></p>

<p>When I joined Cazoo, it was the first place I’d ever worked that explicitly, actively, aggressively embraced software craftsmanship. Pair programming. Test-driven development. Domain-driven design. Extreme programming. The whole kitchen sink. They sent us on agile training courses that a startup founder would weep at the cost of. We had an agile coach in the room every day. We did code katas regularly.</p>

<p>And even there, in the most craft-soaked environment I’d ever been in, the idea of 100% code coverage was treated as obvious lunacy. A poor metric. The kind of thing only someone who hadn’t really understood testing would chase.</p>

<p>Then I joined the Economist, and the team I landed on had 100% coverage as a hard rule.</p>

<p>While they did write tests, they didn’t do TDD. They didn’t pair. They hadn’t been on the agile bootcamps. They hadn’t done code retreats or code katas. By every measure of either the London or Chicago school of craftsmanship tradition would care about, they were doing less of the work. But they had the 100% rule, and they enforced it, and at first I assumed they’d inherited a metric without fully understanding it.</p>

<p>They hadn’t. Turns out I hadn’t understood it. And by the time I left that team, I’d come around entirely. Not reluctantly, not with caveats, but genuinely: 100% coverage, properly understood, is mandatory. I held that position for years before agentic coding was a thing anyone was thinking about. The agents haven’t changed my mind. They’ve just taken a position I already held and made the case for it screamingly, urgently obvious in a way it previously wasn’t.</p>

<h2 id="what-is-this-metric-thing-about-anyway">What is this metric thing about anyway?</h2>

<p>Here’s what I’d absorbed from the craft world about 100% coverage. It’s a vanity number. Chasing it produces garbage tests. You end up writing assertions against getters and setters. You exercise code without testing behaviour. The pragmatic position, and pragmatism was always the emphasis, is that you write the tests that matter and you let the rest go.</p>

<p>All of that is true if “100% coverage” means “every line has a test exercising it.” That version of the metric is genuinely silly and the people warning against it were right.</p>

<p>But it took me until very recently to notice was that nobody, in all those arguments, had ever actually explained what the metric was <em>for</em>. What it was pointing at. Everyone, including me, was arguing about the number. Nobody was asking what the number was a proxy for.</p>

<p>It’s a proxy for <strong>Conscious Coverage</strong>. That’s the thing. Every line in the codebase is a decision. The question the metric is actually asking, underneath, is: <em>have you made a conscious decision about each one</em>. Not have you tested each one. Have you <em>decided</em> about each one. Tested, or consciously chosen not to test, with a reason, written down.</p>

<p>Concretely, it looks like this. You write a function with a branch that handles a malformed input. You run the coverage tool. It tells you the error branch isn’t covered. You now have three choices, and only three.</p>

<ol>
  <li>You can write a test that exercises the malformed input and asserts the behaviour.</li>
  <li>You can mark the branch ignored with a comment that says, say, “unreachable because upstream validation guarantees this shape” — and now your justification is a reviewable artefact that someone can argue with in a pull request.</li>
  <li>Or you can decide the branch shouldn’t exist at all and delete it. What you cannot do is shrug and move on. The forgotten case is no longer a thing. Every line has had a decision made about it, and the decisions are legible.</li>
</ol>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">...</span>
  <span class="k">static</span> <span class="nx">countryToRegion</span><span class="p">(</span><span class="nx">countryCode</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">Region</span> <span class="p">{</span>
    <span class="cm">/* v8 ignore start */</span> <span class="c1">// Ignoring the switch to avoid repeating every single country code</span>
    <span class="k">switch</span> <span class="p">(</span><span class="nx">countryCode</span><span class="p">)</span> <span class="p">{</span>
  <span class="p">...</span>
</code></pre></div></div>

<p>Once you see that, the version of the rule that the craft world rejected and the version the Economist team was running are obviously different things. The first one optimises for a number. The second one optimises for <em>the absence of accidents</em>. You can no longer fail to test something because you forgot. You can fail to test it because you decided not to, and you wrote down why, and someone can argue with you about it later in the review. The shape of the work is different.</p>

<p>And this is the bit I have to be honest about, because the post doesn’t work without it. Once the metric is framed as conscious coverage, the pragmatic position I’d absorbed at Cazoo stops being pragmatic. It’s just laziness with a vocabulary. “Write the tests that matter and let the rest go” sounds wise until you ask which lines, specifically, didn’t matter, and why, and the answer turns out to be that I didn’t want to write those tests and the tradition had given me a way to sound rigorous about not writing them. The metric wasn’t too expensive. The work it pointed to wasn’t too expensive. I just didn’t want to do it, and nobody was making me, and the craft vocabulary let me call that a considered trade-off.</p>

<p>I had to be in a place that just <em>did</em> it before I could see any of this. Sitting at Cazoo arguing about it from first principles, I would have lost the argument every time, because the version of the rule I was arguing against was the version everyone agrees is bad, and the version underneath it, the one about conscious, nobody had ever put into words for me. Nobody tells you the better version exists until you’re standing inside a codebase that runs on it.</p>

<h2 id="what-changes-when-an-agent-is-doing-the-writing">What changes when an agent is doing the writing</h2>

<p>Fast forward. I’m now writing a lot of code with agents. Claude Code, Codex, OpenCode, the usual suspects. The thing I keep telling people who ask me about it is that agentic engineering requires <em>more</em> discipline than normal engineering, not less. The tools are faster, the output is bigger, and the gaps between what you asked for and what you got are easier to miss. So everything that used to depend on careful human attention now depends on something else holding the line. Which brings me back to the question: how do I know it’s done? And more importantly, how does an agent know?</p>

<p>Not “done” in the user-acceptance sense. Done in the much more boring sense of: has this thing actually exercised the code it claims to have written? Has it tested the behaviour I care about? Did it quietly skip a branch because the test was annoying to set up? Did it write something that’s technically passing but structurally untestable?</p>

<p>These are the questions the craftsmanship tradition spent twenty years building intuitions about, and the answer the tradition arrived at, pragmatically, contextually, with appropriate caveats, was mostly “you’ll know it when you see it, and pairing helps, and code review helps, and time helps.” Which is fine when humans are doing the work at human pace. It is not fine when an agent has just produced four hundred lines in ninety seconds and is asking what to do next.</p>

<p>The agent needs a guard rail. Something machine-checkable. Something it can run, get a number from, and decide for itself whether to keep going. Something another agent can validate.</p>

<p>100% coverage, in the conscious sense, turns out to be exactly that. The agent finishes its loop, runs the coverage tool, sees 98%, and knows, without me telling it, that there are two percent of decisions it hasn’t made yet. Either write the test, or mark the lines as ignored with a justification. Both are fine. What’s not fine is leaving the gap.</p>

<p>And here is where the impact of the reframe gets outsized, because the agent doesn’t have my laziness. The agent doesn’t want to go home. The agent isn’t quietly negotiating with itself about which lines it can get away with skipping. The thing that was always standing between me and conscious coverage, which was me, just isn’t there. The metric stops being a rod I have to hold myself to and becomes a rod the agent holds itself to, cheerfully, at four in the morning, forever. The practice the craft tradition argued about most fiercely for human reasons becomes, for agents, the most natural thing in the world.</p>

<p>I’ve started using this as one of my standard acceptance criteria. “You are done when coverage reports 100%.” I can kick off a thirty-minute task and come back to something that, whatever else is true of it, will at least be testable, and will at least have had every line consciously decided about.</p>

<p>Coverage as the gate at the end works better when there’s a process upstream that’s likely to produce decent tests in the first place. If you set up the harness with CLAUDE.md files that push the agent toward red-green-refactor TDD, and you give it the kind of structured prompting (like obra/superpowers) that shapes how it actually approaches a task, you tilt the odds. There’s no guarantee it’ll write tests first. There’s a much better chance it will, and a much better chance the tests it writes are pulling the design rather than chasing it. That upstream tilt plus the downstream gate is a much sturdier system than either piece on its own.</p>

<p>There’s a sharpening of all this that matters, though, because coverage on its own can still produce tests that exercise code without actually testing anything. The companion practice, and I’d say it’s a necessary one rather than a complementary one, is writing tests outside-in, from behaviour rather than from structure. Test the unit of behaviour, not the unit of code. Don’t mock the internals; let the real thing run and assert against what the user of the code actually cares about. This was already the right answer when humans were writing the tests, because it produces tests that survive refactors and read like documentation. With agents it becomes critical, because a behaviour-shaped test is one the agent can write legibly from a user story, and one that you, as the reviewer, can read and check against intent without having to trace the implementation. Coverage tells you the agent made a decision about every line. Behavioural framing tells you the decisions were about the right things. You need both. Coverage without behavioural framing is theatre; behavioural framing without coverage leaves gaps you’ll find in production.</p>

<p>Now for the obvious objection. Agents are world-class metric gamers. They will absolutely write meaningless tests that exercise code without asserting anything useful. They will absolutely mark lines as ignored with justifications like “this branch is unreachable” when the branch is, in fact, reachable. If you treat 100% coverage as a number to satisfy, the agent will satisfy the number and you’ll be worse off than before, because now you have a green build hiding a problem instead of a red one announcing it.</p>

<p>The reason I think it works anyway is that it’s asking the right question of the metric. Coverage, in the conscious sense, is a completeness check. It tells you every line has had a decision made about it. It was never going to tell you the decisions were good ones. That’s a different question, and it wants a different answer. Behavioural tests, written outside-in from what the user of the code actually cares about, are the correctness check. Mutation testing, which flips operators and boundaries and asks whether any test notices, is the check on whether the assertions are doing real work. The gaming the agent does lives in the gap between those checks, and the mitigation isn’t to make coverage smarter. It’s to stop asking coverage to do correctness’s job. Use it for what it is: a completeness gate that makes the decisions visible. Use behavioural framing and mutation testing for the quality of the decisions. The ignored lines and their justifications are, at least, a reviewable artefact, sitting in one place where you can read them. The cheats are confined to a place you’re looking. None of that is automatic. It’s a discipline, and like every guard rail it collapses the moment you stop maintaining it. The question is whether the rail makes problems easier or harder to spot, and I think this one makes them easier.</p>

<h2 id="the-truisms-didnt-go-away">The truisms didn’t go away</h2>

<p>The craft tradition produced a lot of practices, and a lot of arguments about practices, and a lot of nuance about when practices apply. Most of that nuance was about humans. About the cost of the practice to the person doing it, about whether the discipline was worth the friction, about whether the metric would be gamed. A lot of it, and I say this now having lived on both sides of the argument, was about whether the person doing the work would actually do it if you asked them to.</p>

<p>Agents don’t have that problem. The friction of writing the extra test isn’t a friction the agent feels. The discipline of marking ignored lines with reasons isn’t a discipline the agent has to be talked into. The kind of metric-gaming that comes from a tired human at five-to-six is replaced by a different kind of gaming, which is its own problem. So practices that were borderline-worth-it for humans become straightforwardly worth it for agents, and practices that were rejected as lunacy for humans turn out, on inspection, to have been rejected for reasons that said more about the humans than about the practice.</p>

<p>The craft was always about building software in a sustainable, predictable, maintainable way. That hasn’t changed. The agents don’t replace the craft. They inherit it. And some of the practices the tradition argued about most fiercely turn out, in this new context, to be exactly the load-bearing ones. Not because the old arguments were wrong about the metric, but because the old arguments were quietly also about us, and the us part has changed.</p>

<p>100% coverage wasn’t wrong. It was a proxy for something nobody I knew named. That allowed me to point at work I didn’t want to do, and dressed up in a vocabulary that let me agree with myself about not doing it. The agents don’t have the vocabulary and don’t need it. Which makes me wonder which other practices were rejected for reasons that were really about us, and what the calculation looks like now that we have a collaborator who just, straightforwardly, does the work. I’ve run that calculation for coverage. I’m increasingly sure it isn’t the only practice the answer flips for. I’d quite like to know which others.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="aios" /><category term="claudecode" /><category term="softwarecraftsmanship" /><summary type="html"><![CDATA[We don’t talk about Code coverage, no no no, we don’t talk about coverage…]]></summary></entry><entry><title type="html">The Canary in the Harness</title><link href="https://www.petervanonselen.com/2026/04/12/the-inevitable-lobotomisation-of-claude/" rel="alternate" type="text/html" title="The Canary in the Harness" /><published>2026-04-12T08:00:00+00:00</published><updated>2026-04-12T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/04/12/the-inevitable-lobotomisation-of-claude</id><content type="html" xml:base="https://www.petervanonselen.com/2026/04/12/the-inevitable-lobotomisation-of-claude/"><![CDATA[<p><em>On discovering that your favourite tool got measurably worse, that you’d been blaming yourself for it, and that the only reason you noticed at all was because another harness was sitting right next to it behaving normally.</em></p>

<hr />

<p><img src="/assets/canary-hero.png" alt="The Canary in the Harness" /></p>

<h2 id="a-tale-of-two-ralph-loops">A tale of two Ralph loops</h2>

<p>A couple of weeks ago I was playing with Newshound, a personal project of mine that pulls together a digest of interesting things from a list of about thirty sources on the internet. I wanted to add a feature that was a little more involved than the usual yak shave. Spec conversation. PRD skill. JSON. Ralph loop. The full ceremony.</p>

<p>I ran the loop in Claude Code. It went for two hours. A good chunk of that two hours was Claude Code recursively chewing on the same problem, half-finishing things in slightly different ways each time around. Eventually it limped over the finish line. At which point my Pro subscription tapped out.</p>

<p>I went off and set up the wrapper script from <a href="https://www.petervanonselen.com/2026/04/11/the-grand-plugin-trap/">the last post</a> to allow me to run a Ralph loop on OpenCode. I then ran the <em>exact same prompt</em> through OpenCode with GPT-5.4. Same Ralph loop. Same PRD. Same instantiation of the problem.</p>

<p>Fifteen minutes.</p>

<p>I noticed this. Of course I noticed this. And the conclusion I reached, the one anyone would reach, was: huh, GPT-5.4 must just be better at this particular kind of task. I filed it under “interesting data point about model personalities” and moved on. I’d written about how each harness has its own character in <a href="https://www.petervanonselen.com/2026/04/03/the-council-will-see-you-now/">the council post</a>, and this felt like more of the same. Different tool, different shape, sometimes one fits the keyhole better than the other. Cool.</p>

<p>That was the wrong conclusion. I just didn’t know it yet.</p>

<h2 id="what-newshound-put-on-my-desk">What Newshound put on my desk</h2>

<p>Two days ago Newshound surfaced <a href="https://github.com/anthropics/claude-code/issues/42796">a GitHub issue</a> on the Claude Code repo. There is a particular pleasure in your own tool catching the thing that’s about to reframe how you think about your other tools, and I want to note it before I move on, because the whole point of personal projects is moments like this.</p>

<p>The issue was filed by Stella Laurenzo, an engineer working deep in the AMD GPU compiler stack on IREE. Not a casual user. Not someone shouting into the void about vibes. Someone whose day job is to run dozens of concurrent Claude Code agents against a non-trivial systems codebase, who logs everything, and who knows how to do statistics to data.</p>

<p>The headline finding is brutal. From late January through early March, she analysed 17,871 thinking blocks and 234,760 tool calls across 6,852 Claude Code session files. What she found is that somewhere between mid-February and early March, Claude Code’s behaviour changed in measurable, reproducible, machine-readable ways.</p>

<p>The number that broke me is the Read:Edit ratio. In the good period, Claude Code was reading 6.6 files for every file it edited. By mid-March, that ratio had collapsed to 2.0. The model stopped reading code before changing it. One in three edits in the degraded period was made to a file the model hadn’t read in its recent tool history.</p>

<p>There’s more. A “stop hook” she built to programmatically catch Claude trying to dodge work, ask unnecessary permission, or declare premature completion fired 173 times in seventeen days. It had fired zero times before March 8th. Zero. Every phrase in that hook was added in response to a specific incident where Claude tried to stop working and had to be forced to continue. The word “simplest” in Claude’s outputs went up by 642 percent. The word “please” in <em>her</em> prompts dropped 49 percent. The word “thanks” dropped 55 percent. She stopped being polite to it because there was nothing left to be polite about.</p>

<p>The methodology is more rigorous than anything I would ever bother to do, the dataset is enormous, and the appendix where Claude Opus analyses its own session logs and writes “I cannot tell from the inside whether I am thinking deeply or not” is one of the more haunting things I’ve read in a technical bug report.</p>

<p>Go and read it. I’m not going to recap the whole thing. The point that matters for this post is much smaller and much more personal.</p>

<h2 id="the-thing-id-been-blaming-on-myself">The thing I’d been blaming on myself</h2>

<p>I have been using Claude Code since June last year. In that time it has been, without much competition, the most enjoyable engineering tool I’ve ever used. The blog you’re reading exists in part because of how much I have wanted to write about working with it.</p>

<p>But over the last few weeks something had been off. Sessions felt slower. The chatter I was used to, the running commentary where Claude Code would talk through its plan as it worked, had gone quieter. The two-hour Ralph loop on Newshound was the loudest version of it but it wasn’t the only one. I’d had a couple of sessions where it felt like Claude was rushing to a conclusion, where the reflection phase produced shallower answers than I was used to, where I was correcting more and praising less.</p>

<p>I had put all of this down to me. I’d been burnt out and needing a holiday. I was probably tired. I was probably prompting badly. The problem was probably harder than I’d estimated. The Ralph loop was probably a poor fit for the task. GPT-5.4 was probably just better at this particular slice of work.</p>

<p>None of those things are unreasonable explanations. They’re the kinds of explanations a senior engineer reaches for first, because the alternative, “the tool I rely on every day got measurably worse without telling me,” feels paranoid and slightly embarrassing. So you eat it. You assume the variable that changed is you.</p>

<p>And then someone with 6,852 session logs and a Pearson correlation coefficient publishes the receipts, and you sit there reading them on a Sunday afternoon thinking: oh. Oh, that’s what that was.</p>

<h2 id="the-argument-the-council-post-wasnt-making-yet">The argument the council post wasn’t making yet</h2>

<p>When I wrote about <a href="https://www.petervanonselen.com/2026/04/03/the-council-will-see-you-now/">convening multiple AI harnesses as an architectural review council</a>, the pitch was about getting better answers. Different harnesses have different personalities, the harness matters more than the model, three opinions plus a synthesis beats one opinion. All of that I still believe. But there was a second argument hiding in there that I didn’t see at the time, and Stella’s report is what dragged it into the light.</p>

<p>Multi-harness working is regression detection.</p>

<p>It is, for most of us, the <em>only</em> regression detection we are ever going to have. I am not going to instrument my Claude Code sessions, capture 234,760 tool calls, and run a signature-length correlation against thinking depth. I have a day job and a stealth tactics game to build. Stella did that work and the rest of us are in her debt for it, but it is not a repeatable practice for anyone whose job title isn’t “compiler engineer with infinite patience and a logging fetish.”</p>

<p>What <em>is</em> repeatable is keeping three harnesses in active rotation and noticing when one of them starts feeling off relative to the other. The fifteen-minutes-versus-two-hours moment with Newshound was a regression signal. I just didn’t read it as one because I had no framework for the idea that the harness itself might be the variable. I assumed harnesses were stable. They are not stable. They are moving targets, reconfigured continuously by people who do not write to you about what they changed, and the only way you find out is by holding two of them up to the same problem and watching one of them flinch.</p>

<p>This is what the plugin trap was protecting against without me fully understanding why. <a href="https://www.petervanonselen.com/2026/04/11/the-grand-plugin-trap/">Yesterday’s post</a> was about keeping the exits visible so you don’t get locked into a single ecosystem. The thing I didn’t say, because I didn’t know it yet, is that the room you’re standing in is being remodelled while you sleep. Exits aren’t just for when you want to leave. Exits are how you find out the room has changed shape.</p>

<p>If your entire workflow lives inside one harness, harness drift is invisible to you. It just feels like you’re having a bad week. You blame yourself. You prompt harder. You write longer CLAUDE.md files. You assume the problem is on your side of the screen, because from inside one harness there is no other side of the screen to compare against.</p>

<h2 id="naming-names-because-this-is-supposed-to-be-honest">Naming names, because this is supposed to be honest</h2>

<p>I am going to name Claude Code directly here, because this blog only works if I’m being truthful about what I’m actually using.</p>

<p>The tool that got measurably worse over the last month is Claude Code. The tool I have loved more than any other engineering tool in the last decade is Claude Code. Those two sentences belong in the same paragraph. I am writing this <em>because</em> of how much I like the thing, not in spite of it.</p>

<p>If you have been feeling like Claude Code is harder to work with than it was in February, you are probably not imagining it, and you are probably not getting worse at your job. There is data. The data is good. Go and read it.</p>

<h2 id="what-im-taking-away">What I’m taking away</h2>

<p>Three things, and then a rabbit hole.</p>

<p>First, I want crude metrics on my own harness usage. Not 234,760-tool-call-Pearson-correlation crude. Just crude. How many tool calls per session. How many file reads versus file edits. How many times I had to interrupt and correct. Even a daily tally of “did Claude Code feel like it was trying today” would be more signal than I currently collect, which is zero. If the regression signal is detectable in aggregate, I want to be looking at the aggregate.</p>

<p>Second, I want a smoke-test prompt suite. A handful of canonical prompts that exercise the kinds of work I actually do, that I can run across harnesses on a rough cadence and use as a tripwire for drift. Nothing fancy. A small fixed battery, run weekly, results scribbled in a notebook. The point is not the rigour, the point is the comparison over time. I have been operating without a baseline and it has cost me.</p>

<p>Third, the portability argument from the plugin trap post upgrades from “useful insurance against rate limits and lock-in” to “the only way you will ever notice that your tools have silently changed underneath you.” Multi-harness working is the canary. If your canary is the same species as the thing you’re trying to detect, you don’t have a canary. You have another bird in the same mine.</p>

<p>And then the rabbit hole.</p>

<h2 id="the-next-room-over">The next room over</h2>

<p>There is a project called <a href="https://pi.dev">pi</a> by a developer named Mario Zechner. The tagline on the front page is “There are many coding agents, but this one is mine,” which is doing a lot of work in eight words. Pi is a minimal, aggressively extensible terminal coding harness. The pitch is that you adapt pi to your workflow rather than the other way around. No sub-agents, no plan mode, no built-in todos, no MCP, no permission popups, no background bash. All of those things are extensions you add, or build, or install from someone else’s package. The core stays small and the shape comes from you.</p>

<p>There is <a href="https://www.youtube.com/watch?v=Dli5slNaJu0">a YouTube video</a> by Mario walking through how he came to build it that I have not yet found the time to fully watch, and this post is partly me giving myself permission to find that time.</p>

<p>The reason pi feels like the natural next thing is that it is the logical endpoint of an argument I’ve been making in pieces across the last few posts. The plugin trap post said your workflow shouldn’t live inside one harness. The council post said different harnesses give you different answers. This post is saying different harnesses give you the only honest baseline you have for spotting drift in any one of them. The next move, the move I cannot stop thinking about, is: what if the harness itself is something you own? What if instead of being a tenant in three different rooms, all of them being remodelled by other people on different schedules, you build a small room of your own, with the doors where you want them, and treat the rented rooms as the comparison set?</p>

<p>I do not know yet whether pi is the right answer to that question. I have not run it. I have not watched the video. I have a game and a new digest agent I am supposed to be working on, and the smell of yak around me is already pretty thick.</p>

<p>But I can feel the next dive coming. And after the week I’ve just had, I am done pretending that holding still inside a single harness is the safe choice. The safe choice is having somewhere else to look from.</p>

<p>Off I go.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="aios" /><category term="claudecode" /><category term="softwarecraftsmanship" /><summary type="html"><![CDATA[On discovering that your favourite tool got measurably worse, that you’d been blaming yourself for it, and that the only reason you noticed at all was because another harness was sitting right next to it behaving normally.]]></summary></entry><entry><title type="html">The Grand Plugin Trap</title><link href="https://www.petervanonselen.com/2026/04/11/the-grand-plugin-trap/" rel="alternate" type="text/html" title="The Grand Plugin Trap" /><published>2026-04-11T08:00:00+00:00</published><updated>2026-04-11T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/04/11/the-grand-plugin-trap</id><content type="html" xml:base="https://www.petervanonselen.com/2026/04/11/the-grand-plugin-trap/"><![CDATA[<p><em>A modest meditation on plugins, portability, and the peculiar sorrow of a workflow that cannot leave the building.</em></p>

<hr />

<p><img src="/assets/grand-plugin-trap/hero.png" alt="hero hotel" /></p>

<p>It’s day two of my holiday and I’m staring at a Claude Code session that won’t do anything. Pro limit hit. Three days until it resets. There’s a personal project sitting open in another window that I’d been quite enjoying poking at, and now I can’t poke at it, and the bit of my brain that had been having a perfectly nice time is suddenly very loud about the £20 of extra credit I’d burned through in a single afternoon earlier in the week.</p>

<p>This is the story of how that lockout forced me to do a small piece of unglamorous setup work I’d been avoiding for months, and what I found on the other side of it.</p>

<h2 id="the-workflow">The workflow</h2>

<p>Quick context. Over the last nine or ten months I’ve fallen into a working rhythm with my personal projects that goes something like this. I open an AI chat, and I have a long conversation with it. Not a “write me some code” conversation. A “let’s interview each other about what I’m actually trying to build and why” conversation. These run for three or four hours sometimes. Lots of back and forth, lots of poking at scope, lots of trying to find the smallest version of the thing that would actually tell me whether the idea is any good. At the end of all that I have what I’ve been calling a spec: a high-level document about what we’re doing and why.</p>

<p>Then I take the spec and run it through a PRD skill I shamelessly stole from the Ralph loop. Quick aside: PRD is a term I had genuinely never encountered in fifteen years of working in agile teams. I first heard it watching YouTube videos about people working with AI, sometime in the last year, and I had to go and look up what the bloody hell it stood for. As best I can tell, a PRD is an epic with a collection of user stories, some acceptance criteria, some functional and non-functional requirements, and a bit of product context bolted on top. Cool. I can work with that. The reason I like this particular PRD skill is that after I’ve already spent four hours on the spec conversation, it asks me five more questions to validate what I’m building. Which is exactly the kind of thing you want at that stage!</p>

<p>PRD becomes JSON. JSON gets fed to a Ralph loop. Off we go.</p>

<h2 id="the-bit-where-i-was-cheating">The bit where I was cheating</h2>

<p>Here’s the dirty secret. I’d never actually set up the Ralph loop the way you’re supposed to set it up. I’d been running it via a plugin inside Claude Code. Plugins are wonderful. You install them, they work, you’re productive in ninety seconds. Why would you write a bash script when you can install a plugin?</p>

<p>The honest answer is: you wouldn’t. <em>And that’s the trap. The problem isn’t plugins. The problem is when your workflow only exists inside one of them.</em></p>

<p>Plugins feel like the harness rewarding you for committing to it. Every plugin install is a small vote for staying inside that one ecosystem, and those votes compound quietly until one day you look up and notice you’ve stopped being portable. You’re not running a workflow anymore. You’re running a workflow <em>that only exists inside Claude Code</em>. Which is fine, until it isn’t.</p>

<h2 id="how-i-burned-through-the-credits-in-the-first-place">How I burned through the credits in the first place</h2>

<p>I should be clear about something. I hadn’t hit the Pro limit doing serious work on my personal project. I’d hit it because it was my holiday, and I’d spent the previous week happily down an oh-my-codex rabbit hole for no reason other than that it was interesting.</p>

<p>Oh-my-codex is a sprawling wrapper that someone has built around Codex to give it brainstorming flows and Ralph loops and a pile of other usability niceties. I’d become curious about it for a very specific reason: when the Claude Code source leaked, a developer in South Korea used Codex with oh-my-codex to reimplement the entirety of Claude Code in Python. In six hours. <em>Six hours</em>, for a non-trivial codebase. I wanted to understand how that was even possible, which meant I wanted to make oh-my-codex work with OpenCode and Claude Code rather than just Codex, because of course I did. More harnesses. Always more harnesses.</p>

<p><img src="/assets/grand-plugin-trap/the-way.png" alt="the way" /></p>

<p>So that’s what the credits went on. A week of trying to bend an already-baroque wrapper around two more harnesses it wasn’t designed for, purely because I wanted to know how the thing worked. No deliverable. No project at the end of it. Just the kind of dive-in-and-poke-at-it exploration that holidays are for. I was having a great time, the plugin inside Claude Code was still humming along for the actual personal project I dipped into between rabbit hole sessions, and the cost of any of this hadn’t shown up yet.</p>

<p>Then it showed up.</p>

<h2 id="the-thing-id-been-ignoring-at-work">The thing I’d been ignoring at work</h2>

<p>I should have seen this coming, because at The Economist I have access to three different coding agents with three different usage pools, each gated on different constraints. In practice that means I bounce between them all day. Hit a five-hour window in one, switch to another, work until that one taps out, switch to the third. It’s a genuinely lovely setup if you’re the kind of person who likes being spoiled for choice on tokens.</p>

<p>But it also means I’ve been quietly reinstalling the same plugins and the same markdown scripts in three different places, every time something changes. And whenever one of those environments goes down or gets reconfigured, I lose half a morning rebuilding the workflow in another one. I’d been feeling that friction for ages without ever quite naming it. It was just background noise. The cost of doing business.</p>

<p>Then the personal Pro lockout happened, and suddenly the background noise was the only thing in the room.</p>

<p><img src="/assets/grand-plugin-trap/darkness.png" alt="darkness" /></p>

<h2 id="doing-the-unglamorous-thing">Doing the unglamorous thing</h2>

<p>So I went and found <a href="https://github.com/Th0rgal/open-ralph-wiggum">open-ralph-wiggum</a>, worked out how to wire it up properly, and wrote <a href="https://github.com/vanonselenp/zsh-functions/blob/main/functions/ralph-loop.zsh">a small zsh function</a> that wraps it so I can just type <code class="language-plaintext highlighter-rouge">ralph-loop</code> from any project directory and have the thing kick off without me having to remember any flags. None of this was hard. None of this was interesting. It was the kind of work I had been actively avoiding because I’d already spent a week earlier that month fiddling around with Codex and OpenCode and trying to make various things play nicely together, and the last thing I wanted was <em>more</em> yak shaving.</p>

<p>But here’s the thing about doing it during a forced lockout, with nothing else to distract me. There was nothing else to do. So I sat with it. And once it was done, I had a Ralph loop that ran on top of OpenCode, with GPT-5.4, completely independent of whether Claude Code was up, down, or rate-limited into oblivion. The wrapper meant I could move between harnesses without rebuilding anything. The script lived in my dotfiles. It was just <em>there</em>.</p>

<h2 id="the-real-prize">The real prize</h2>

<p>I’ve <a href="https://www.petervanonselen.com/2026/04/03/the-council-will-see-you-now/">written before</a> about how each AI harness has its own personality. Claude Code thinks differently from Codex thinks differently from OpenCode, and a lot of that personality lives in the harness rather than the model. I still believe that. But what I hadn’t fully clocked, until this week, is that everything I’d written about harness personalities was manual copy paste painful exercises, because the plugin had me boxed into one of them.</p>

<p>Knowing the council exists is one thing. Being able to actually convene it on a Tuesday afternoon while you’re trying to ship something is another. The wrapper script is the thing that closes that gap. It allows for more meaningful agentic workflows in any harness easily.</p>

<p>That’s the prize. Not the lockout workaround. Not the bash script. The portability that lets the multi-harness thing actually be a way of working rather than an essay.</p>

<h2 id="what-im-sitting-with">What I’m sitting with</h2>

<p>I’m going to keep using plugins. They’re genuinely useful and I’m not about to LARP as someone too principled to install convenient things. But I’m going to be more suspicious of how easy they make the first ninety seconds feel, because I now have a much clearer sense of what they cost on the back end. Every plugin ecosystem is a small gravity well. The more you commit, the harder it is to leave, and, this is the part that bothers me most, the less you can even see what you’re missing on the outside.</p>

<p>The unglamorous wrapper script turns out to be a small act of resistance against that. Not a heroic one. Just a vote for keeping the exits visible.</p>

<p>I’d rather have the exits visible.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="aios" /><category term="claudecode" /><category term="softwarecraftsmanship" /><summary type="html"><![CDATA[A modest meditation on plugins, portability, and the peculiar sorrow of a workflow that cannot leave the building.]]></summary></entry><entry><title type="html">The Council Will See You Now…</title><link href="https://www.petervanonselen.com/2026/04/03/the-council-will-see-you-now/" rel="alternate" type="text/html" title="The Council Will See You Now…" /><published>2026-04-03T08:00:00+00:00</published><updated>2026-04-03T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/04/03/the-council-will-see-you-now</id><content type="html" xml:base="https://www.petervanonselen.com/2026/04/03/the-council-will-see-you-now/"><![CDATA[<p><em>You were the chosen one! You were supposed to destroy the hallucinations, not join them!</em></p>

<hr />

<p><img src="/assets/council/council.png" alt="The council" /></p>

<p>I use multiple AI agents as an architectural review council. When I said that out loud recently, I got the look. You know the one. The polite nod that says “I have no idea what you just said but I’m going to smile and move on.”</p>

<p>So here’s the footnote.</p>

<h2 id="the-setup">The setup</h2>

<p>I’m currently juggling two things at work that have no business being juggled at the same time.</p>

<p>The first is a set of deeply tangled bugs that have been lurking since July 2024. They’re tied to a third-party integration, they’re interconnected, and we’re only now seeing the full scope of how bad they are. This is slow, careful, multi-day investigation work. Long-form conversations with AI. Reading code. Writing tests to verify behaviour. Building the case for “this is exactly where the problem is and this is exactly why.”</p>

<p>The second is supporting our engineering manager to enable an external contracting team to deliver a new feature in our codebase. The contractors have never touched our code before. They don’t have a clear picture of the requirements, the systems, or how everything interacts. The depth they need to make meaningful architectural decisions is broad, deep, and nuanced. I’ve worked in the systems, but I don’t have enough depth to answer all their questions off the top of my head.</p>

<p>But what I do have is access to Claude, Claude Code, GitHub Copilot, and Codex.</p>

<h2 id="the-council-assembled">The council, assembled</h2>

<p>I should back up. For months now, in my personal life, I’ve been asking the same questions to ChatGPT, Gemini, and Claude interchangeably. I call them my council. Each one has a different personality, notices different things, and observes different angles. I find that when I’m getting multiple opinions I make better decisions. This is just me applying the same instinct to my workspace.</p>

<p>It started with a simple question: we have a third-party payment provider that offers a payment method, and we have a number of integrations between us and them. The contracting team needed to understand how we use it. How do we integrate with backend services? Where are the bits that are backend-for-frontend versus actual backend platform services? How do all of these systems interact? Where are all the endpoints?</p>

<p>I spent a day and a half in long-form conversations with multiple AI systems interrogating the problem. I started at the web-facing entry point and worked backwards. I trawled Confluence, Slack, Google Drive, and every other form of long-term documentation to build a picture of what the contracting team was going to need. Then I took all of that context, the goals of the team, and the documentation, and used it to structure a comprehensive prompt.</p>

<p>I ran that prompt through three different AI harnesses: Codex, Claude (via the web), and Claude Code running Opus. Each one went away, investigated the same repositories, and came back with structured answers. Then I took those structured answers, went and explored the code myself, used the hints they’d given me to validate everything, and wrote up a comprehensive document explaining the lot.</p>

<h2 id="naive-me-thought-job-done">Naive me thought job done</h2>

<p>Obviously I was not done. You’d think by now I’d know better.</p>

<p>A week later the contracting team came back with “cool, we have a plan.” They’d taken everything I’d given them, created architectural diagrams, Confluence documents, and actual thinking. They wanted me to review whether their approach would work.</p>

<p>So I did the same thing again. Took their documents, the context I’d already built, and pointed all three AI harnesses at the relevant repositories, not just mine but across four different teams’ repos, backend services and frontend services and everything in between. I had each one validate whether what the contractors were proposing would actually work. Then I took the outputs from all three, wrote them to file, and had a fourth agent (OpenCode running Opus 4.6) synthesise a combined result. I used that synthesis to structure my response back to the team.</p>

<p>I’ve now done this process three times. Here’s how it works:</p>

<blockquote>
  <p><strong>The Council Process</strong></p>

  <ol>
    <li>Gather context and documentation</li>
    <li>Structure a comprehensive prompt</li>
    <li>Run the same prompt through multiple AI agents and let each investigate the repositories independently</li>
    <li>Save their structured outputs</li>
    <li>Run a synthesis agent across all results</li>
    <li>Validate manually: use the AI outputs as a map for where to look, read the code yourself, and run quick targeted questions past other engineers</li>
  </ol>
</blockquote>

<h2 id="where-the-real-value-lives">Where the real value lives</h2>

<p>The synthesis step is where the magic happens. It’s not just about getting three answers. It’s about what happens when you put them next to each other.</p>

<p>The synthesising agent highlighted where all three harnesses were in agreement, which gave me confidence. But more importantly, it highlighted where they’d noticed different pieces of the problem. Even though they were looking at the same repositories and most likely using the same underlying tools, they ended up pulling out different things. Codex might flag an endpoint I hadn’t considered. Claude Code might trace a data flow the others glossed over. The breadth of coverage from running three agents was meaningfully wider than any single one.</p>

<p>This also feeds into something that should be obvious but bears repeating: AI hallucinates. You cannot 100% commit to trusting just one version. When you need accurate architectural understanding, having multiple agents give you a synthesis that you then validate yourself is genuinely useful. It’s not a replacement for reading the code. It’s a way to read the code faster and know where to look.</p>

<h2 id="the-tools-have-personalities">The tools have personalities</h2>

<p>Here’s something I find fascinating, and I’m not the only one. Another principal engineer I know has noticed the same thing.</p>

<p>Codex is the grumpy pragmatist. It goes away, gets some stuff done, comes back, and tells you the bare minimum you need to know. Not a single detail more. Bullet points, to the letter, done. That’s fine. That’s exactly what you’d expect from a tool optimised for task completion.</p>

<p>Claude, given the exact same prompt via the web with Opus, comes back reading like a chatty engineer. A bit scattered, a bit flowery, but thorough. You’ll get everything you need, it’ll just need a bit of back-and-forth to extract it cleanly.</p>

<p>But here’s the interesting bit: OpenCode, regardless of which model it’s running underneath, whether that’s Opus or GPT-5.4, tends to give better structured results than either of the first-party platforms running the same models. The investigations are better organised. The outputs are clearer. The intent comes through more directly. I’m finding this is true when comparing Claude Code to OpenCode running Opus, and it’s also true when comparing Codex to OpenCode running GPT-5.4.</p>

<h2 id="the-harness-matters-more-than-the-model">The harness matters more than the model</h2>

<p>The scaffolding around the model, how it structures tool calls, how it formats its output, how it organises an investigation, is doing more heavy lifting than people assume. That’s a genuinely counterintuitive finding. The same model, in a different harness, produces meaningfully different quality of output. If you’re only evaluating models, you’re missing half the picture.</p>

<h2 id="ai-expands-capacity-not-energy">AI expands capacity, not energy</h2>

<p>I’ll be honest. By the end of every week right now, I am flattened. Exhausted. Mentally, emotionally, everything. Gone.</p>

<p>These tools have enabled me to explore and understand systems at a superficial level far faster than I ever could otherwise. To get the depth I needed for these handover documents would have taken weeks of investigation. I did it in hours. That’s real. That’s meaningful. That capacity expansion let me keep working on high-priority deep-dive bugs while simultaneously supporting an external team and ensuring they had enough context to be unblocked and start working independently.</p>

<p>But working with AI at full tilt is cognitively expensive in ways that people underestimate. You’re doing more, faster, and that uses more of your mental energy than you think. AI gave me the capacity to do work that would’ve been impossible to fit in otherwise. It did not give me more energy to do it with.</p>

<h2 id="still-experimenting">Still experimenting</h2>

<p>I’ve run this playbook three times now without changing it. Same process each time. I haven’t tried to refine it or automate it or build a harness around it yet, though it’s in the back of my mind. I actually started building a mobile app a while back to formalise the council concept for personal use, but got distracted because, well, reasons.</p>

<p>So what’s the lesson? Don’t trust one AI. Use a council. Get them to validate each other. Get them to look for reasons they’re wrong. Use the synthesis of multiple perspectives to build confidence in your understanding, then go validate it yourself.</p>

<p>I’m still not entirely convinced this is the best strategy. But it is letting me do things I could not have done otherwise, and right now that’s enough. The future of AI-assisted engineering might not be a better model. It might be a better council.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="aios" /><category term="claudecode" /><category term="softwarecraftsmanship" /><summary type="html"><![CDATA[You were the chosen one! You were supposed to destroy the hallucinations, not join them!]]></summary></entry><entry><title type="html">The Smell of Panic When You Context Thrash</title><link href="https://www.petervanonselen.com/2026/03/24/the-smell-of-panic-when-you-context-thrash/" rel="alternate" type="text/html" title="The Smell of Panic When You Context Thrash" /><published>2026-03-24T08:00:00+00:00</published><updated>2026-03-24T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/03/24/the-smell-of-panic-when-you-context-thrash</id><content type="html" xml:base="https://www.petervanonselen.com/2026/03/24/the-smell-of-panic-when-you-context-thrash/"><![CDATA[<p><em>High high hope for the code, shooting for a PR when I couldn’t even make a commit…</em></p>

<hr />

<p><img src="/assets/panic.png" alt="Panic at the keyboard" /></p>

<p>Over the past few weeks I have been increasingly panicked. That’s probably the only honest way to frame it.</p>

<p>I’ve been juggling a lot. Supporting a team building out a new payment method. Handing over deep knowledge of an existing payment method to another team working outside our scope but integrating with systems we own. Helping yet another team figure out how to break a backend platform service from a monolith into microservices. And somewhere in between all of that, trying to actually deliver a feature myself: a seemingly straightforward change to one of our frontend purchase journeys to make it faster.</p>

<p>Each of those things individually requires serious context. Together they’ve been chewing up my headspace and pulling me in every direction. Which means that whenever I finally sat down to write actual code, I arrived in a state of absolute panic. The “oh my gosh I have so little time, move quickly, move quickly, move quickly” kind of survival mode.</p>

<p>And that’s when I made one of the most fundamental mistakes you can make with AI-assisted development.</p>

<h2 id="the-50-file-disaster">The 50-File Disaster</h2>

<p>Here’s the thing about AI-generated code: it’s easy. So easy that it’s almost impossible to remember just how easy it is, because you’ve spent a lifetime handcrafting code yourself. You carry this default assumption that building things is complicated and slow.</p>

<p>So I did what I thought was the right thing. I planned. I looked through the existing code. I thought about it. I wrote out detailed acceptance criteria. I thought about the problem from the AI’s perspective. And then I said “cool, I think I have enough” and started implementing.</p>

<p>By the time I got someone else to look at it, they pointed out it was missing a key behaviour. I’d misunderstood part of the acceptance criteria. OK, fine. I started trying to fix it.</p>

<p>And then trying to fix it. And trying to fix it.</p>

<p>What was a small hole became a deep hole became a nightmare became “why does it feel like I will never, ever get anywhere with this?” By the end of it I was touching something like 50 files across two repos. Small changes scattered everywhere, most of them not even really needed. All driven by the panic override of needing to get something done while constantly being pulled out of context and back in and out and back in until I was just thrashing. Burning cycles. Making zero progress.</p>

<h2 id="panic-is-a-smell">Panic Is a Smell</h2>

<p>If you’ve been in software long enough you know what a code smell is. Something that isn’t technically broken but tells you something deeper is wrong.</p>

<p>The panic to get things done is a smell. The pushing and pushing and pushing is a smell. The feeling that you don’t have enough time, that you have to ship something right now, that you can’t afford to slow down? That’s a smell. And I ignored it for way too long.</p>

<p>Because here’s the lesson I apparently need to keep re-learning: with AI-assisted development, <em>the writing of code is not the bottleneck</em>. It never was. The understanding is the bottleneck. And when you’re panicking, you skip the understanding to get to the doing, which is exactly backwards.</p>

<h2 id="the-reset">The Reset</h2>

<p>I eventually stopped. Stepped away from the mess. Started with a brand new repository. Took all the things I’d learned, the plan document, the acceptance criteria, everything. And then I spent an entire day in conversation with an AI. Not writing code. Just investigating.</p>

<p>Testing existing behaviour. Running multiple examples and execution paths. Making sure I had a precise, clear understanding of what the current system actually did, what the new behaviour needed to be, and all the various paths between them. I literally spent hours asking the AI to explain each step of its plan and justify why it chose that approach.</p>

<p>I say all the time that planning matters more than coding. But experiencing the contrast firsthand is different. Hours of slow, methodical, back-and-forth investigation. Deep thinking about context. Deep thinking about what you’re trying to do and why. So that when you, <em>the human in the loop</em>, actually ask the AI to build something, the full context of what you’re trying to achieve is sitting clearly in your head. You understand the user behaviour. The system interactions. The high-level architecture. You could draw all the diagrams because you actually understand what needs to be done.</p>

<p>The feature that had consumed a week and a half of thrashing? After that day of planning, it took a couple of hours to get something working correctly.</p>

<h2 id="the-council-of-ais-or-going-wide">The Council of AIs (or: Going Wide)</h2>

<p>Meanwhile, on the other side of my work life, I’ve been doing something completely different with AI tooling.</p>

<p>To support the contracting team building out a new feature, I’ve been running what is essentially a council of AIs to review their design documents. OpenCode, Codex CLI, and Claude Code running simultaneously so I can verify, validate, and cross-compare. Deep-dive analysis with Claude and ChatGPT for architectural decisions and historical context. Complex investigation into bugs that were first logged two years ago and never properly resolved.</p>

<p>I have been holding the context of a massive amount of different workstreams. Work that would have taken me days or weeks to get even a baseline understanding of. The AI tooling genuinely lets you go wide in a way that wasn’t possible before.</p>

<p>And that’s where the tension lives.</p>

<h2 id="shield-and-sword">Shield and Sword</h2>

<p>The honest truth is that I’ve been doing two very different jobs at the same time.</p>

<p>One job is the shield: absorbing context, running investigations, unblocking other teams, reviewing designs, holding the big picture so nobody else has to. The AI tooling makes this possible. It lets you hold 10x the context. You can pre-empt meetings by using Claude to pull together context and solve problems before the meeting even happens, cancelling two or three in a morning and buying yourself hours of uninterrupted time. You can run parallel investigations across multiple tools and hold the full picture of what’s going on across an entire programme of work.</p>

<p>The other job is the sword: actually sitting down and delivering a piece of working software. And that requires the opposite of going wide. It requires going deep. Slow. Methodical. Boring, even.</p>

<p>The AI enables both.</p>

<p>But your brain can’t do both at the same time.</p>

<p>When you try, you thrash. You burn cycles switching between deep and wide, and just like a thrashing computer, you end up doing a lot of work and making no progress.</p>

<h2 id="what-im-taking-away">What I’m Taking Away</h2>

<p>Two things, and they’re in tension with each other, and I’m OK with that.</p>

<p><strong>Go deep before you go fast.</strong> Planning with AI isn’t just “write a spec and hand it over.” It’s hours of investigation. It’s asking the AI to explain its own plan in painful detail. It’s making sure you understand the problem so well you could solve it by hand. The code is the easy part. The understanding is the work.</p>

<p><strong>AI lets you hold a lot of context, but your brain still has limits.</strong> Context switching costs the same as it always did. Maybe more, because the AI makes it tempting to take on everything. You can hold the shield and the sword, but not at the same time. Deliberately buying yourself blocks of deep time is not optional. It’s the whole game.</p>

<p>This is the new trap for senior engineers. AI lets you take on more surface area than ever before. But the work that actually ships still requires deep focus. Nobody is immune to thrashing, no matter how good the tooling gets.</p>

<p>And if you’re sitting there right now, pushing and pushing and panicking and feeling like you’ll never get there? That’s a smell. Stop. Step away. Start again with understanding, not urgency.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="aios" /><category term="claudecode" /><category term="softwarecraftsmanship" /><summary type="html"><![CDATA[High high hope for the code, shooting for a PR when I couldn’t even make a commit…]]></summary></entry><entry><title type="html">The Feedback That Doesn’t Care About Your Title</title><link href="https://www.petervanonselen.com/2026/03/17/the-feedback-that-doesnt-kill-you/" rel="alternate" type="text/html" title="The Feedback That Doesn’t Care About Your Title" /><published>2026-03-17T08:00:00+00:00</published><updated>2026-03-17T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/03/17/the-feedback-that-doesnt-kill-you</id><content type="html" xml:base="https://www.petervanonselen.com/2026/03/17/the-feedback-that-doesnt-kill-you/"><![CDATA[<p><em>What doesn’t kill you makes you stronger … right?</em></p>

<hr />

<p>I’ve been writing this blog for a while now. I’ve documented scope creep spirals, the joy of deleting code I spent weeks writing, and the slow painful education of learning to work with AI agents without letting them run off a cliff. If you’ve been following along, you know the theme by now: I learn things the hard way and then write about it so you don’t have to. Or at least so you can watch.</p>

<p>Yesterday I was explaining to a colleague how I use Gemini to give me personalised feedback on my performance after meetings. He’s a staff engineer, someone who’s genuinely deep into AI-assisted coding, not a tourist. And even he stopped and went: “Wait, you’re doing <em>what</em>?”</p>

<p>That reaction made me step back. Because I hadn’t really sat down and thought about what I’d actually built over the past year. I’d been solving problems one at a time, better code generation here, better context gathering there, a way to get honest feedback on myself over here, and somewhere along the way it had become something bigger. A system. A layer underneath how I work that I now can’t imagine working without.</p>

<p>So this is me trying to describe what that looks like, now that I’ve finally noticed it.</p>

<p><img src="/assets/feedback-that-kills-you/image.png" alt="the layers" /></p>

<h2 id="the-code-layer-get-off-the-autocomplete">The code layer: get off the autocomplete</h2>

<p>If you’re writing code with an AI assistant inside your IDE, Copilot in VS Code for instance, I’d gently suggest you’re missing the better experience. CLI agents like Claude Code, Codex CLI, and Open Code have fundamentally changed how I interact with code. Open Code has become my go-to because it works well straight out of the box and, crucially, it connects to Copilot’s backend models. If your company already pays for Copilot, Open Code might be the unlock you didn’t know you were waiting for.</p>

<p>The shift matters because it changes what the AI is doing. Inside an IDE, it’s autocomplete with delusions of grandeur, guessing what you want line by line. On the command line, I’m describing problems, analysing architecture, pulling systems apart, generating diagrams. It stops being a typing assistant and starts being a thinking partner.</p>

<p>I use this for everything that touches code directly: writing it, reviewing it, debugging, breaking things apart to understand them, building architecture diagrams. The lot.</p>

<h2 id="the-context-layer-taming-the-organisational-scatter">The context layer: taming the organisational scatter</h2>

<p>Every engineer in a large organisation knows this pain. The information you need to do your work lives in seven different places: Slack threads, Google Meet recordings, Confluence pages, Jira tickets, GitHub PRs, and at least two places nobody told you about. You spend half your time assembling a coherent picture before you can even start thinking.</p>

<p>ChatGPT and Claude’s enterprise integrations have changed this for me. Both allow you to connect to corporate tools, your chat platform, docs, issue tracker, source control, and pull context into a single conversation. Instead of trawling through three Slack channels and two Confluence pages for forty minutes, I pull it all together and ask: what does this ticket actually need? What are the acceptance criteria? What am I missing?</p>

<p>Here’s where it compounds. Good acceptance criteria from this layer mean better prompts for the coding agents. The layers feed each other. I didn’t design it that way, it just happened once the pieces were in place.</p>

<h2 id="the-mirror-feedback-that-doesnt-care-about-your-feelings">The mirror: feedback that doesn’t care about your feelings</h2>

<p>This is the hard one to talk about. And the one that made my colleague stop in his tracks.</p>

<p>We’re a remote organisation. Google Meet is where everything happens, and Gemini sits inside every meeting. Most people don’t turn on transcriptions, which I think is a mistake. Any meeting producing collective knowledge should generate a transcript. Those transcripts feed the context layer above.</p>

<p>But there’s another use that took me months to work up to.</p>

<p>After meetings where I’m an active participant, I ask Gemini: as a staff engineer, what went well, what didn’t go well, and what can I improve on?</p>

<p>The first few times, it was rough.</p>

<p>Here’s the thing about feedback from humans: you almost never get anything useful. You either get “yeah, that was fine” or something so carefully hedged that whatever kernel of truth was in there has been sanded down to nothing. I’ve rarely received feedback that was specific, actionable, and tied directly to something I actually did in a real moment.</p>

<p>Gemini doesn’t do hedging. It references specific things that happened in the meeting. “You reframed the argument here and it shifted the conversation constructively.” Or: “You weren’t listening here and this is where it cost you.” It once told me that while I’d handled a frustrated colleague well, I could have spotted the frustration earlier and intervened before it escalated, and that when another colleague was dismissive, I’d recovered well but could have prepared for that reaction. Specific. Contextual. Minutes after it happened.</p>

<p>When I explained this to my colleague yesterday, he asked: “Isn’t this just seeking perfection?” And I realised, no. It’s just a way to learn and grow and become more deliberate about how I communicate, how I lead, and how I interact with the people around me. You can’t improve what you don’t measure. This is measuring.</p>

<p>But here’s what really made me think this is bigger than my own little experiment. I told a principal engineer friend at another company about this approach. He had a difficult conversation coming up, recorded it with Gemini, and afterwards used the transcript to get actionable feedback on how he’d handled it. His reaction was genuine shock. He’d never had that clear a picture of how his conduct was landing. An engineering manager I know has started doing the same thing and describes it as brutal but the most meaningful feedback he’s received in years.</p>

<p>And I think there’s a reason for that. I remember chatting with a startup CEO at a meetup who made the observation that the higher you go in leadership, the less honest feedback you receive. The position of power makes it hard for people to cross that barrier. Gemini doesn’t have any concept of your title or your seniority. It just tells you what it saw.</p>

<p>In the beginning, every session felt like a wake-up call. After months of doing this consistently, keeping a log, reading it back, it softened. Not because the feedback got less honest, but because the gap between what I thought I was doing and what I was actually doing got narrower. Fewer surprises. More gentle nudges, fewer gut punches.</p>

<h2 id="so-what-is-this-actually">So what is this, actually?</h2>

<p>None of these tools alone would be worth a blog post. A CLI coding agent is nice. Enterprise AI integrations save time. AI self-reflection is powerful but weird. What caught me off guard, what I only noticed yesterday when I saw my colleague’s reaction, is that they work as a system.</p>

<p>Meeting transcripts feed the context layer. The context layer produces better acceptance criteria. Better acceptance criteria drive better output from the coding agents. The self-improvement loop makes me more effective in the meetings that generate the transcripts. Each layer feeds the others. I didn’t plan it. I just kept solving problems and the connections emerged.</p>

<p>There’s a Sam Altman interview from about a year ago where he describes people using AI as “an operating system for how they think.” At the time I had absolutely no idea what he meant. Now I think I do, and the uncomfortable truth is that I’m probably barely scratching the surface of where this goes.</p>

<p>So here is my take away action for you. Next meeting you are in with a transcript, ask an LLM for some honest feedback. Let me know if you learn anything interesting!</p>

<p>I’m still figuring it out. As usual, you’ll hear about it when I do.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="aios" /><category term="claudecode" /><category term="softwarecraftsmanship" /><summary type="html"><![CDATA[What doesn’t kill you makes you stronger … right?]]></summary></entry><entry><title type="html">How I Learnt to Stop Worrying and Love Agentic Katas</title><link href="https://www.petervanonselen.com/2026/03/05/learn-to-love-agentic-coding/" rel="alternate" type="text/html" title="How I Learnt to Stop Worrying and Love Agentic Katas" /><published>2026-03-05T08:00:00+00:00</published><updated>2026-03-05T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/03/05/learn-to-love-agentic-coding</id><content type="html" xml:base="https://www.petervanonselen.com/2026/03/05/learn-to-love-agentic-coding/"><![CDATA[<p><em>I don’t know how to teach this. But I think I’ve figured out how to practice it…</em></p>

<hr />

<p>Have you ever struggled to get started with something new, not because the thing itself is hard, but because the <em>shape</em> of how to learn it isn’t clear? That’s where I’ve been stuck with agentic coding. Not the doing of it. I’ve been doing it for months. The teaching of it. The “how do I help someone else get started” of it.</p>

<p>And then I remembered code retreats. And katas. And the way I actually learned TDD all those years ago, not from a book, but from structured practice with low stakes and room to play.</p>

<p>So I built a set of agentic katas: structured coding exercises designed specifically for practising AI-assisted development. Not traditional katas, those are too small and the AI already knows all the answers. These are bigger, meatier problems in unfamiliar domains that force you to engage with the full process of working alongside an agent.</p>

<p>Let me explain how I got here.</p>

<h2 id="a-brief-history-of-practising-on-purpose">A Brief History of Practising on Purpose</h2>

<p>Early in my career, code retreats were the thing that taught me test-driven development. Not a book. Not a course. A structured, all-day event where you solve Conway’s Game of Life over and over again, each time with different constraints. Maybe your pair is actively trying to <em>not</em> solve the problem. Maybe you’re strictly ping-ponging. Maybe you delete your code every 45 minutes.</p>

<p>The point was never to solve Conway’s Game of Life. The point was to internalise the patterns and practices of TDD by giving yourself a safe space to experiment. No production pressure. No deadlines. Just play.</p>

<p>Code katas grew out of the same ethos, small self-contained problems that shouldn’t take more than an hour or two. The algorithm doesn’t matter. How you choose to solve it does. They’re bite-sized by design. They’re not supposed to be hard. They’re supposed to be <em>practice</em>.</p>

<h2 id="the-problem-with-katas-and-ai">The Problem with Katas and AI</h2>

<p>Here’s the thing I’ve been struggling with: traditional code katas don’t work for learning agentic development. They’re too small. The LLM has already seen every solution to FizzBuzz and Roman Numerals in its training data. You’re not practising a workflow, you’re watching an AI regurgitate a known answer. There’s nothing to explore, nothing to plan, no decisions to make about approach or tooling.</p>

<p>And that matters, because the skill you need to develop with agentic coding isn’t “how to prompt an AI to write code.” It’s how to <em>think alongside one</em>. How to explore a problem space together. How to write a plan that gives an agent enough context to be useful. How to verify that what came back is actually what you wanted. How to set up your workspace so the AI has the right guardrails.</p>

<p>None of that shows up in a 30-minute kata where the AI already knows the answer.</p>

<h2 id="the-accidental-discovery">The Accidental Discovery</h2>

<p>What’s funny is that I’ve kind of been doing agentic katas already, almost by accident. I just didn’t realise it at the time.</p>

<p><img src="/assets/agentic-kata/kata1.png" alt="The first kata" /></p>

<p>A few weeks ago I wrote about <a href="https://www.petervanonselen.com/2026/02/10/agentic-play/">deleting code on purpose as a way to recover from burnout</a>. I’d been experimenting with the Ralph Wingum loop, throwing PRDs at an agentic coding workflow, seeing what came out, then deliberately throwing the code away. The output I was chasing wasn’t a codebase. It was understanding. How big can a PRD get before the loop breaks? How much do agent files matter? What’s the minimum setup to get something useful?</p>

<p>Each run was a contained experiment. Fresh repo, clear problem, focused practice, delete the code, do it again. I was varying one thing at a time, adding a CLAUDE.md file, scaling up the PRD size, trying a different domain, and learning from each iteration.</p>

<p>I was doing agentic katas. I just hadn’t named them yet.</p>

<p>Looking back, the whole arc has been building toward this. My game project was the first rough version, months of cycling through spec-driven development and making mistakes. The “delete the code” experiments compressed that into focused sessions. And now, formalising the structure into something other people can pick up feels like the obvious next step.</p>

<h2 id="building-the-thing">Building the Thing</h2>

<p>So I’ve put together a set of agentic katas. The idea is that each one should require somewhere in the region of four to eight hours of focused hand crafted work to do <em>well</em>. And by “well” I mean the full golden plate: test-driven, 100% coverage, clean README, proper git history, the works. Not because the output matters, but because doing that level of work with an AI agent forces you to actually engage with the process.</p>

<p>The loop for every kata is the same:</p>

<p><strong>Explore</strong> → <strong>Plan</strong> → <strong>Set Up Context</strong> → <strong>Build</strong> → <strong>Verify</strong></p>

<p>And there’s one key rule that makes the whole thing work: <em>you are not allowed to choose a programming language or framework until you’ve had a conversation with your AI tool about what the best approach is.</em></p>

<p>This is the rule that forces the shift. Instead of jumping straight to “build me X in TypeScript,” you have to start with “I need to solve this problem, what are my options?” You explore the problem space. You figure out what tools exist. You have the AI challenge your assumptions. <em>Then</em> you decide on an approach.</p>

<p>From there, you write a detailed plan, acceptance criteria, example data, use cases, a breakdown into small chunks of work. You set up your workspace with an agent file and think about what context to include. You build incrementally. And you verify everything: read the plan, read the code, run it, test it, confirm it does what you intended.</p>

<p><img src="/assets/agentic-kata/agentic-kata-loop.svg" alt="the loop" /></p>

<h2 id="the-katas-themselves">The Katas Themselves</h2>

<p>I’ve started with four problems, each chosen because they sit in a domain most developers haven’t worked in before:</p>

<p>An <strong>audio transcriber</strong> that handles speech-to-text with timestamps and speaker diarisation. A <strong>background remover</strong> for image segmentation. A <strong>meme generator</strong> that deals with text rendering and positioning on arbitrary images. And a <strong>thumbnail ranker</strong> that scores images for visual appeal.</p>

<p>Each kata has deliberate ambiguity baked in, because real problems are ambiguous, and part of the skill is figuring out what questions to ask. They also have a privacy constraint (everything runs locally, no cloud APIs for processing) and an extra credit extension for when you want to push further.</p>

<h2 id="why-this-matters-right-now">Why This Matters Right Now</h2>

<p>I’ll be honest: the reason I’m putting this together is partly selfish. I want to run a workshop with my colleagues, and I need structured material to do it. But there’s a bigger motivation too.</p>

<p>Right now, everything about AI and software development feels incredibly intense. Fear of being made obsolete. AI layoff discourse everywhere. The pressure to have strong opinions about tools you’ve barely had time to evaluate. It’s all very stressful, and stress is the enemy of learning.</p>

<p>What people actually need, what <em>I</em> needed, and what I accidentally created for myself, is a safe space to play. A contained environment where you can try things, make mistakes, and build intuition without the stakes of production code or career anxiety hanging over you.</p>

<p>Code retreats gave us that for TDD. I’m hoping agentic katas can do the same for working with AI.</p>

<h2 id="the-repo">The Repo</h2>

<p>I’ve put everything together in a repo: <a href="https://github.com/vanonselenp/agentic-katas">github.com/vanonselenp/agentic-katas</a></p>

<p>It includes the kata briefs, a participant guide covering the rules and process, and a facilitator guide for anyone who wants to run this as a structured session with their team. A session takes about 90 minutes.</p>

<p>I haven’t run this with anyone else yet. I only put it together today, and I’m planning to trial it with my team in the coming weeks. It might be brilliant. It might be terrible. Either way, I’ll write about how it goes.</p>

<p>But the core idea, that you need bigger, unfamiliar problems to practise AI-assisted development, and that the process matters more than the output, that I’m confident about. Because I’ve been living it, accidentally, for months.</p>

<p>If you try it, I’d love to hear how it goes. And if you’re doing something different to build these skills, I’d love to hear about that too.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="claudecode" /><category term="specdrivendevelopment" /><category term="katas" /><category term="softwarecraftsmanship" /><summary type="html"><![CDATA[I don’t know how to teach this. But I think I’ve figured out how to practice it…]]></summary></entry><entry><title type="html">14 PRs, 6 Repos, 1 Button: A Tale of Tumbling Down the Rabbit Hole</title><link href="https://www.petervanonselen.com/2026/02/12/rabbit-holes/" rel="alternate" type="text/html" title="14 PRs, 6 Repos, 1 Button: A Tale of Tumbling Down the Rabbit Hole" /><published>2026-02-12T08:00:00+00:00</published><updated>2026-02-12T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/02/12/rabbit-holes</id><content type="html" xml:base="https://www.petervanonselen.com/2026/02/12/rabbit-holes/"><![CDATA[<p><em>True stories from the front lines of the internet…</em></p>

<hr />

<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Down_the_Rabbit_Hole_%28311526846%29.jpg/960px-Down_the_Rabbit_Hole_%28311526846%29.jpg" alt="Alice falling down the rabbit hole" />
<em>Alice in Wonderland by <a href="https://commons.wikimedia.org/wiki/File:Down_the_Rabbit_Hole_(311526846).jpg">Valerie Hinojosa</a> / <a href="https://creativecommons.org/licenses/by-sa/2.0/">Creative Commons Attribution-Share Alike 2.0
</a></em></p>

<p>Now this is a story all about how one button link got my codebase flipped turned upside down. And I’d like to take a minute, just sit right there, and I’ll tell you how I shipped 14 PRs without pulling out my hair.</p>

<p>It started with a Monday morning meeting. I’d been off for three weeks. The meeting was dense with context about decisions made months ago, documented across scattered specs and design docs. Systems I don’t own. Plans originally speced out almost a year prior. SEO requirements. Legacy middleware behaviour. And somewhere in all of this, a single task: change where a subscribe button points.</p>

<p>The old flow routed users through a legacy auth endpoint which was a piece of middleware handling user state and return-to-site functionality. The new flow should skip that layer and go direct. Simple, right?</p>

<p>Three repos. 3 small PRs. That was the original scope.</p>

<p>It became six repos and one or two more PRs…</p>

<h2 id="the-context-problem">The Context Problem</h2>

<p>Here’s what made this tricky: I didn’t have the context. Not the institutional knowledge of why things were built this way. Not the codebase familiarity to know where all the tendrils reached. Not the cross-system visibility to see how changes would ripple.</p>

<p>Normally, this is where you’d involve other teams. Schedule alignment meetings. Negotiate architecture choices. Coordinate timed releases. The org chart becomes the constraint.</p>

<p>Instead, I threw five AI tools at the problem.</p>

<p>I used internal knowledge search to surface half a dozen docs from a year ago about what a potential migration might look like. Copilot and Codex scanned repos I’d never opened, outputting high-level analysis of what would need to change. NotebookLM synthesised a dozen-plus sources into actionable Jira tickets with acceptance criteria and testing plans. And Claude handled the actual implementation across all six repositories.</p>

<p>Each tool for what it does best. None of them sufficient alone.</p>

<h2 id="the-shape-of-the-change">The Shape of the Change</h2>

<p>What was supposed to be three repos became six because the AI tooling kept finding rabbit holes worth going down.</p>

<p>The approach was backwards compatibility first. I updated the auth service to forward requests to the new endpoint, so existing systems would keep working. Only after that was stable did I remove the old code paths and switch the calls to point directly to the new flow.</p>

<p>Along the way, I hit a referrer bug that only revealed itself mid-implementation. One of the components lived in a shared library, not a full application, which meant handling referral data differently than expected. This meant that I had to change how it was reading from window referrer data rather than relying on direct redirect URLs.</p>

<p>And then there was a shared header component in another team’s repo. Hardcoded to the old endpoint. In code I couldn’t easily modify. The rabbit holes kept cropping up every time I thought I dived down them all.</p>

<p>Fourteen PRs. Six repositories. Backwards compatible throughout. Zero downtime.</p>

<p>The old flow had an extra hop through legacy middleware that handled state management. The new flow removes that layer entirely. Which makes for a faster time to checkout, same user experience, one less thing to maintain.</p>

<h2 id="the-point">The Point</h2>

<p>This would have been a multi-team effort. Alignment meetings across three teams, at minimum. Negotiated timelines. Architectural discussions. Coordinated releases.</p>

<p>Instead, it was one developer holding context that used to require an org chart.</p>

<p>I’m not saying AI tooling makes you a better engineer. I’m saying it lets you hold more context. And sometimes that’s the difference between “we’ll need to schedule a meeting with the other teams” and “I’ll have a PR up by Thursday.”</p>

<p>The context ceiling just got a lot higher.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="vibecoding" /><category term="claudecode" /><category term="specdrivendevelopment" /><category term="codex" /><summary type="html"><![CDATA[True stories from the front lines of the internet…]]></summary></entry><entry><title type="html">This Is the Way: Delete the Code</title><link href="https://www.petervanonselen.com/2026/02/10/agentic-play/" rel="alternate" type="text/html" title="This Is the Way: Delete the Code" /><published>2026-02-10T08:00:00+00:00</published><updated>2026-02-10T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2026/02/10/agentic-play</id><content type="html" xml:base="https://www.petervanonselen.com/2026/02/10/agentic-play/"><![CDATA[<p><em>How I learned to do AI Katas and make disposable code helped me recover from burnout</em></p>

<hr />

<p>Burnout is real, and it takes time to work its way out.</p>

<p>I spent the last couple of months trying to work up the will to tackle game projects again. Every attempt fizzled. After a 3+ month slog on a project that <a href="https://www.petervanonselen.com/2025/11/20/scope-creep/">refused to reach an end state</a>. I had nothing left.</p>

<p>So instead of committing to another massive project, I started playing.</p>

<p>I came across the Ralph Wingum loop (<a href="https://www.youtube.com/watch?v=RpvQH0r0ecM">30-min video if you’re curious</a>) and decided to experiment. The premise is simple: use two Claude skills and a bash script to let an AI go full agent mode. The first skill, <code class="language-plaintext highlighter-rouge">/prd</code>, takes a spec and generates user stories with verifiable acceptance criteria. The second, <code class="language-plaintext highlighter-rouge">/ralph</code>, converts that PRD into JSON. Then you loop over the JSON until done. This is basic agentic coding, but AI-agnostic and surprisingly effective.</p>

<p><img src="/assets/agentic-play/image.png" alt="the wiggam loop" /></p>

<p>I needed a project to test this on, so I picked <a href="https://boardgamegeek.com/boardgame/163474/v-sabotage">V-Sabotage</a>. It’s a board game I enjoy but rarely get to play (toddler life), and more importantly, it’s simple enough to define a clear MVP: rooms, a player, guards, sneaking mechanics, a win condition. I’d learned my lesson about scope.</p>

<p>The real experiment wasn’t building the game. That was just the head fake. It actually was figuring out how to break down the work. How big should each PRD be? Do you treat each milestone as its own PRD? Do you throw the whole spec at it and see what happens?</p>

<p>I had to find out.</p>

<p><strong>First run:</strong> I threw a PRD at the skills, ralphed it, and looped on a fresh repo. What came out was very familiar from the last time I was building a game in Godot with AI. Bascially something that worked, but buggy, clunky, no tests, poor signal architecture, tightly coupled code.</p>

<p><strong>Second run:</strong> Same PRD, same loop, but this time I initialised the repo with a CLAUDE.md file first. Just basics: test-drive the code, use Godot 4.x best practices, that sort of thing.</p>

<p>The difference was dramatic. The AI wrote its own test runner. It test-drove everything, achieved high coverage, produced cleaner interfaces, used signals properly, and kept things decoupled. Twenty minutes of compute, and the output was genuinely good. Honestly the thing that blew my mind on this was <strong>it wrote it’s own TEST RUNNER!?!?</strong>. Are you kidding me?</p>

<p>So … key lesson: agent files matter. A lot.</p>

<p><strong>Third run:</strong> I embedded full milestones into the PRDs—a dozen user stories each, multiple acceptance criteria. The loop churned through it and produced a testable MVP in surprisingly little time.</p>

<p><img src="/assets/agentic-play/tactics.png" alt="stealth game" /></p>

<p><strong>Fourth run:</strong> I got distracted by an app idea. Spent a couple of hours refining it with AI, generated a chunky PRD, threw the whole thing at the <code class="language-plaintext highlighter-rouge">/prd</code> skill. It produced 40 stories. I looped it. An hour later, with 5% of my usage allowance remaining, I had a working prototype.</p>

<p><img src="/assets/agentic-play/mobile-app.png" alt="random app" /></p>

<p>It didn’t do exactly what I wanted. But it did most of what I’d asked. This was surprisingly more than enough to immediately change my thinking about what I actually needed in a meaningful way.</p>

<p>And here’s the thing that made all of this feel like play instead of work: <strong>I deleted the code.</strong>. I went full <a href="https://www.coderetreat.org/">code retreat conways game of life</a> delete the code.</p>

<p>Multiple times. Deliberately. The output I was chasing wasn’t a codebase. It was understanding. How big can a PRD get before the loop breaks? (Bigger than I expected.) How much do agent files matter? (More than I expected.) What’s the minimum setup to get something useful? (Less than I expected.)</p>

<p>Disposable code meant low stakes. Low stakes meant I could experiment freely. And experimenting freely, it turns out, is how I recover from burnout.</p>

<p>This play has fundamentally changed how I work at The Economist. But that’s a story for next time.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="vibecoding" /><category term="claudecode" /><category term="specdrivendevelopment" /><category term="codex" /><summary type="html"><![CDATA[How I learned to do AI Katas and make disposable code helped me recover from burnout]]></summary></entry><entry><title type="html">Why You Shouldn’t Speedrun a Production Refactor</title><link href="https://www.petervanonselen.com/2025/12/12/speedrunning-prod-refactors/" rel="alternate" type="text/html" title="Why You Shouldn’t Speedrun a Production Refactor" /><published>2025-12-12T08:00:00+00:00</published><updated>2025-12-12T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2025/12/12/speedrunning-prod-refactors</id><content type="html" xml:base="https://www.petervanonselen.com/2025/12/12/speedrunning-prod-refactors/"><![CDATA[<p><em>Learning the hard way that AI makes discipline more important, not less…</em></p>

<hr />

<p>This week has been a mess. I’ve been ill since last Thursday with a cough that’s been both productive and dizzying, which in a rare moment of clarity made me realize that maybe doing personal dev in the evenings is… not ideal. So no game dev tales this week.</p>

<p>Instead, I want to talk about how I nearly torpedoed a production refactor at The Economist a month ago by forgetting the most important lesson I’ve been learning over the past few months: <strong>AI makes discipline more important, not less.</strong></p>

<h2 id="the-spectacular-failure">The spectacular failure</h2>

<p>I’m currently on the e-comm-funnel team working on the checkout pipeline. One of my first projects has been tackling Commerce Services. This is a Go monolith (a language I’d never used before recently) that started as a POC and got productionized. Naturally, it’s a beautiful mess with conflicting APIs doing all sorts of non-cohesive domain things.</p>

<p>My goal: break it into microservices.</p>

<p>So naturally I did what I’ve been practicing with Horizons Edge and started with a spec. I had a very long conversation with Codex, analyzed the repo structure, identified the domains, mapped dependencies. From this chat we produced a solid 10 page high-level plan.</p>

<p>And the first step of that plan was <em>Phase 1: extract common code into a shared library</em>.</p>

<p>And somewhat predictably, here’s where I got clever.</p>

<p>I thought: “I’ve got a detailed spec. Codex knows Go. Let’s just… do the whole thing! Whats the worst that could happen?”</p>

<p>So I did. One massive refactor. Codex happily obliged.</p>

<p>Then I looked at the pull request: <strong>200 files changed in the monolith. 80 files in the new library.</strong></p>

<p><img src="/assets/speedrun-refactor/this-is-fine.png" alt="this is fine, right?" /></p>

<p>And I just stared at it, completely overwhelmed by the obvious question: <em>How the hell am I going to verify this actually works?</em></p>

<p>There was no way I could meaningfully review 280 files of changes. No way I could ask another engineer to do it. No way to be confident this wouldn’t break something subtle in production. I’d just created an unshippable monster.</p>

<h2 id="starting-over-properly-this-time">Starting over, properly this time</h2>

<p>I scrapped the entire thing and started again with an “I need this to be incremental” mindset.</p>

<p>Not just because I wanted to be able to review it, though that’s critical, but because I genuinely believe small releases into production are the right way to work. It should have been my default starting point. Instead, I’m still learning just how disciplined I need to be when working with AI tooling.</p>

<p>The new approach:</p>

<p><strong>First</strong>, I wrote a much more detailed spec for Phase 1 that lived in the new repo. Not just “extract shared library” but an 8-step plan where each step could go to production independently. Start with the absolute minimum: just one joint service with no dependencies. This would validate the CI/CD pipeline, the integration points, everything, with the smallest possible change.</p>

<p><strong>Then</strong>, one step at a time:</p>
<ul>
  <li>Extracted and deploy leaf utilities (logging, validation, middleware)</li>
  <li>Migrated HTTP routing abstractions</li>
  <li>Moved observability and AWS helpers</li>
  <li>Extracted infrastructure components like health checks</li>
  <li>Finally, the component registry</li>
</ul>

<p>At each step: tested, improved coverage, deployed to production, monitored. The existing systems kept running exactly as before.</p>

<p>I followed the same hyper-methodical approach I’ve been using with the game project. Focusing on small scoped MVP slices and incremental delivery. For the actual development, I loaded Codex into a workspace with both repos and had it follow the spec file for each migration. Then validated with Claude in GitHub Copilot, extensive personal review, and eventually team review before each production deployment.</p>

<p>The result: A refactor of a core system touching ~200 files, in a programming language I’m just learning, in a domain I’d just joined, completed over a couple of weeks with zero downtime. No one on the team was blocked or impacted. It just happened quietly in the background.</p>

<h2 id="what-im-taking-away">What I’m taking away</h2>

<p>Two things keep reinforcing themselves across contexts:</p>

<p><strong>First</strong>: AI amplifies your need for discipline. The easier it becomes to generate large amounts of code, the more critical it is to think carefully about scope, verification, and deployment strategy. One-shotting 280 files feels productive in the moment. It’s not. It’s just creating an unshippable mess you’ll have to undo.</p>

<p><strong>Second</strong>: The “what’s the smallest increment that adds value?” mindset pays off everywhere. It saved Horizons Edge when I was drowning in scope creep. It made this refactor safe and reviewable. It’s not just a nice-to-have for side projects … it’s how you de-risk production changes in unfamiliar territory.</p>

<p>Next up is breaking out actual domains into microservices, starting with Identity &amp; User. But that’s a plan for next year, when I’m hopefully no longer coughing my lungs out.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="vibecoding" /><category term="claudecode" /><category term="specdrivendevelopment" /><category term="codex" /><summary type="html"><![CDATA[Learning the hard way that AI makes discipline more important, not less…]]></summary></entry><entry><title type="html">Finally… A Wild MVP Appears</title><link href="https://www.petervanonselen.com/2025/12/04/finally-a-wild-mvp-appears/" rel="alternate" type="text/html" title="Finally… A Wild MVP Appears" /><published>2025-12-04T08:00:00+00:00</published><updated>2025-12-04T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2025/12/04/finally-a-wild-mvp-appears</id><content type="html" xml:base="https://www.petervanonselen.com/2025/12/04/finally-a-wild-mvp-appears/"><![CDATA[<p><em>Three Months to MVP: What I Learned Building a Tactical Card Game with AI…</em></p>

<hr />

<p><img src="/assets/mvp/banner.png" alt="banner" /></p>

<p>It’s been three months since I started trying to make Horizon’s Edge, a tactical turn-based wargame in the sky. And honestly, I’m completely stunned that I even have something that actually … kinda … works.</p>

<h2 id="from-vibe-to-spec">From Vibe to Spec</h2>

<p>When I started this project, I was pure vibe coding. But somewhere in the middle, when I was trying to refactor the UI from a classic RTS/turn-based strategy with a mass of buttons to something entirely driven by card play, vibe coding hit a wall. I couldn’t get AI tooling to cooperate with my loose intuitions. That’s when I started working with specs and it changed my life…. metaphorical life …. but life!</p>

<p><img src="/assets/mvp/cards.png" alt="card driven" /></p>

<p>Writing a clear specification before diving head first into a vibe changed everything. Suddenly the AI had guardrails. It stopped looping in circles. If you want to go deeper on this, I wrote about starting to figure out specs in <a href="https://claude.ai/2025/10/03/chaos-cards-and-claude-copy/">Chaos, Cards, and Claude</a>.</p>

<h2 id="months-of-education">Months of Education</h2>

<p>What I’ve learned so far is this: it’s entirely possible to make working software with AI tools. You can keep momentum even when you’re completely out of energy or time. But the most important thing … the thing that actually matters … is that you have to always verify what the AI outputs. Automated unit tests are your best friend here. A Quality first mindset and thinking about edge cases is fundamental.</p>

<p><img src="/assets/mvp/waveform.gif" alt="waveform generation" /></p>

<p>I’ve done major refactors. I’ve rethought how the game works multiple times. I added procedural generation because it was fun. I figured out how to make the game work with just card play mechanics that… mostly work (much to my surprise). Some refactors were vibe-coded disasters; others were spec-driven and clean. But every single one taught me something about how to work with AI as a tool rather than a replacement.</p>

<h2 id="the-mvp-what-it-took">The MVP: What It Took</h2>

<p>Two days ago I finished the final major system needed to validate the MVP: the victory system. Islands needed to change ownership. Every existing system needed to integrate with that. Core island nodes needed to be targetable from creatures, spells, and abilities. Getting the targeting code to work meant hitting more touch points than I anticipated. A lot of different abilities needed specific ways to target islands, not just creatures. It was more entertaining than expected, but I had a spec, and that spec kept me honest.</p>

<p>I now have two thematic decks that are actually unique. 10 creatures. Infrastructure cards that terraform and change the world. A spell that destroys the world. Card play mechanics that mostly work. A whole horde of nuanced and detailed rules about damage and combat that work.</p>

<p><strong>Prototype done. Well, mostly.</strong></p>

<p>The mechanics are all there. What’s left is UI feedback. Cards need to show their play cost clearly, what they do when discarded, what abilities they have before you play them. I know all this because I’ve spent three months building it. Others won’t. So I’m going to tackle those UI gaps this week, then put this in front of a few people to get real feedback.</p>

<h2 id="now-its-your-turn">Now It’s Your Turn</h2>

<p>I’ve spent the past five months working on this. It started with a Magic the Gathering Pauper Jumpstart cube that just wouldn’t get out of my head, went running headlong into a board game prototype that was way too complicated and barrelled straight into this game. I’ve learned a ridiculous amount. <strong>But here’s what actually matters:</strong></p>

<p>It is possible to make working software with AI. You can make some amazing things with these tools. You can learn as you go. You can ship those crazy ideas you never thought you could.</p>

<p><strong>So here’s what I want:</strong></p>

<p>I want <em>you</em> go out and make your own mistakes. Build something weird. Use AI as a tool, verify everything it does, and then make something great. Make art. Because making art, well, that’s the most human thing we can do.</p>

<p>Tell me what you build. I want to hear about it.</p>

<p>Till then, keep learning.</p>

<div style="position: relative; width: 100%; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" src="https://www.youtube.com/embed/NdQgOLlt8QQ?si=F2rdwci0GcDvNm9R" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>
</div>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="godot" /><category term="vibecoding" /><category term="claudecode" /><category term="specdrivendevelopment" /><summary type="html"><![CDATA[Three Months to MVP: What I Learned Building a Tactical Card Game with AI…]]></summary></entry><entry><title type="html">The Long Road to MVP</title><link href="https://www.petervanonselen.com/2025/11/27/long-road-to-mvp/" rel="alternate" type="text/html" title="The Long Road to MVP" /><published>2025-11-27T08:00:00+00:00</published><updated>2025-11-27T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2025/11/27/long-road-to-mvp</id><content type="html" xml:base="https://www.petervanonselen.com/2025/11/27/long-road-to-mvp/"><![CDATA[<p><em>How I learned to stop overthinking and ship the damn thing…</em></p>

<hr />

<p><img src="/assets/long-road/banner.png" alt="banner" /></p>

<p><a href="https://www.petervanonselen.com/2025/11/20/scope-creep/">Last week I had a sudden but inevitable realisation</a>. Basically my MVP was completely not an MVP but rather a bloated over-built system that I should have been moving to release sooner. Which in hindsight I should have realised… 4 weeks ago. Lesson learned.</p>

<p>So, this week I started with yet another plan where I detailed out a new spec that broke down the exact things I needed to do. And then… I did just that! Everything else is deferred.</p>

<p><strong>Broad plan for MVP:</strong></p>
<ul>
  <li>1 Spell: Meteor - DONE</li>
  <li>1 Victory condition: own all the islands - IN PROGRESS</li>
  <li>2 Unique Decks - TODO</li>
</ul>

<p>The spell was easy. Just another ability, and make it bigger, cost more and have an animation that then removes a massive amount of real estate. Big, flashy, simple.</p>

<p><img src="/assets/long-road/meteor.gif" alt="meteor" /></p>

<p>The victory condition, however, has turned into a far more crunchy problem. Turns out wiring up ownership, health tracking, and win conditions across multiple systems was messier than I thought. I’ve built:</p>
<ul>
  <li>A <strong>game over</strong> screen to start a new game</li>
  <li>Health on the core island node</li>
  <li>A UI display to see that health</li>
  <li>A manager to track who owns the islands and a way to change ownership</li>
  <li>A victory condition that checks each round if someone’s actually won</li>
</ul>

<p>Almost there! The missing piece is creatures targeting the core island and dealing damage to it. Once I wire up that damage pathway, the whole system clicks. That’s what I’m grinding through at the moment.</p>

<p>All that will be left is two decks. Which should be as simple as updating two config files, and I’ll have a playable MVP. Finally.</p>

<p><img src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExOG05YndpM2NxbWIxaDE5N2t0ODg2NDRia29tdWd0OXhoaXViMTFhaSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/dyw4fuAhPaIh0japgg/giphy.gif" alt="light" /></p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="godot" /><category term="vibecoding" /><category term="claudecode" /><category term="specdrivendevelopment" /><summary type="html"><![CDATA[How I learned to stop overthinking and ship the damn thing…]]></summary></entry><entry><title type="html">How Many Times Do You Have to Build Too Much to Learn Scope Creep?</title><link href="https://www.petervanonselen.com/2025/11/20/scope-creep/" rel="alternate" type="text/html" title="How Many Times Do You Have to Build Too Much to Learn Scope Creep?" /><published>2025-11-20T08:00:00+00:00</published><updated>2025-11-20T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2025/11/20/scope-creep</id><content type="html" xml:base="https://www.petervanonselen.com/2025/11/20/scope-creep/"><![CDATA[<p><em>Scope Creep Keeps Teaching Me…</em></p>

<hr />

<p><img src="/assets/scope-creep-november/banner.png" alt="banner" /></p>

<h1 id="scope-creep-keeps-teaching-me">Scope Creep Keeps Teaching Me</h1>

<p>Have you ever had one of those weeks where you suddenly have to be traveling a lot and don’t really have any time for personal projects? This has been my week. I still managed to get two creatures and six abilities created.</p>

<p>The Flux Chaos controls the board by moving creatures into and out of position, swapping them from behind enemy lines or randomly teleporting them somewhere (maybe even off into the void). The Flux Storm does massive area damage while locking down the area preventing anyone from teleporting in and out. Watching these creatures interact with the existing systems is delightful. The abilities are beginning to feel like they synergize and interact together nicely.</p>

<p><img src="/assets/scope-creep-november/chaos-storm.png" alt="chaos and storm" /></p>

<p>I’ve been on a mission to prototype this strange game idea for a while now. I started the repo on September 4th. Three months later, I’m watching a prototype that’s actually starting to feel like a game with interesting systems.</p>

<h2 id="but-heres-what-i-should-have-learnt-sooner-i-didnt-cut-far-enough">But here’s what I should have learnt sooner: I didn’t cut far enough.</h2>

<p>When I started, I aggressively removed things. All 8 envisioned factions. The single-player campaign, stories, and lore. AI players. Multiplayer. Different form factors like mobile. Complex models and animations. I thought I’d solved the scoping problem. Oh how innocent I was.</p>

<p>I then built ten creatures with twenty-nine abilities. I added procedural island generation and dynamic road construction. I went down tangents and rabbit holes as I iterated from RTS-style UI to pure card-driven gameplay.</p>

<p>Looking at that list now, that should have been a warning sign. And it gets worse: my remaining feature list for MVP includes five more buildings, five more spells, multiple win conditions, fog of war, proper turn behavior, UI polish.</p>

<p>It’s only now that I am looking at my remaining feature list (I wrote up the list for this very blog post), that I realize the entirely obvious thing. I am <em>still</em> overscoping. Scope creep doesn’t stop after one round of cuts. It’s persistent. Even when you’re actively trying to think in MVP terms, there’s a pull to add one more building, one more spell, one more system. Each one feels necessary. But they compound.</p>

<p>I should have picked this up sooner: that grinding feeling I’ve been having with the creatures and abilities. It has felt like motivation leeching away in the never ending drudgery. It was a clear warning sign I was off the path.</p>

<p>So here’s my actual MVP: one spell (meteor), Fog of War, one win condition, UI, and two decks. That’s it. Just the core systems missing, playable, shippable. This is what I should have built from the start, the smallest thing that proves the concept works.</p>

<p><img src="/assets/scope-creep-november/simple.webp" alt="simple" /></p>

<h2 id="the-lesson-keeps-teaching-itself">The lesson keeps teaching itself</h2>

<p>I keep learning the same lesson over and over. I thought I’d solved scope creep when I cut factions and campaigns. Three months later, with ten creatures and twenty-nine abilities built, I realized I’d just learned the lesson at a different scale. Every time I ship something, I understand a little better what actually mattered. And every time, I realize I could have cut further.</p>

<p>The game is starting to have shape because I’ve been building and learning. But I’m also learning, again, that shipping something small and real beats shipping something comprehensive and theoretical. Time to cut deeper. This time, I’m shipping the prototype before I convince myself I need more.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="godot" /><category term="vibecoding" /><category term="claudecode" /><category term="specdrivendevelopment" /><summary type="html"><![CDATA[Scope Creep Keeps Teaching Me…]]></summary></entry><entry><title type="html">How to Get Things Done When You Have Nothing but Process</title><link href="https://www.petervanonselen.com/2025/11/13/the-no-good-low-energy-week/" rel="alternate" type="text/html" title="How to Get Things Done When You Have Nothing but Process" /><published>2025-11-13T08:00:00+00:00</published><updated>2025-11-13T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2025/11/13/the-no-good-low-energy-week</id><content type="html" xml:base="https://www.petervanonselen.com/2025/11/13/the-no-good-low-energy-week/"><![CDATA[<p><em>The tale of how a good System can carry you through the dark times…</em></p>

<hr />

<p><img src="/assets/no-energy-week/banner.png" alt="banner" /></p>

<p>This week I had zero motivation. Motivation tank completely empty.</p>

<p>My goal was 4 creatures and all of the remaining abilities. I only manage to build 2 creatures and 7 abilities instead.</p>

<p>That happening was astonishing to me.</p>

<p>But it did. And the reason isn’t hard work or willpower, it’s process. The structure on how to work with AI that I follow is robust enough to produce good work even when I have nothing left. That’s the real story.</p>

<h2 id="how-the-structure-holds-up">How the structure holds up</h2>

<p>My process is straightforward: compact the conversation, use the spec to guide the next iteration, verify the implementation, have the AI explain what it did, then manually test each ability. Rinse and repeat.</p>

<p>The spec-driven approach kept context alive, for me <em>and</em> the AI. While my motivation was completely drained, the AI kept building. I basically did the bare minimum each day before switching to Marvel Spider-Man, but the structure meant I never lost the thread. I was running on <a href="https://www.youtube.com/shorts/mVQ1bzd816I">Seinfeld method</a> momentum: just keep the chain unbroken.</p>

<p>But here’s the critical bit: <strong>verify everything the AI makes</strong>. The AI will confidently insist “Everything works perfectly!” and update your docs without being asked. Then you test it. Nothing works. You point it out and it turns out half the feature wasn’t there to start with. Point it out again, and more unfinished work is found. The AI is capable and also will miss the obvious. Trust but verify.</p>

<p><strong>Pro tip</strong>: never ever let the AI update the docs without being directly asked to.</p>

<p>This verification habit isn’t just bug-catching. It is the key habit that keeps the process from collapsing. Without it, you’re just building on sand. With it, even a low-energy week produces solid work.</p>

<h2 id="where-designs-actually-get-good">Where designs actually get good</h2>

<p>The most valuable phase in my workflow is when the AI explains what it just built. That’s where I stop and think: does this ability actually do what I want? Should it shift? Should it do something different?</p>

<p>That’s where the magic happens.</p>

<p><img src="/assets/no-energy-week/tank.png" alt="tank" /></p>

<p>The Biomass Tank’s parasitic bond is a perfect example. It started as a triggered ability, shifted into an activated passive that heals the Tank whenever <em>any</em> nearby enemy creature takes damage. That single design decision cascaded: upkeep suddenly mattered. Rounds had weight. Generator buildings became far more valuable, and vulnerable. The creature went from decent to a potential powerhouse with two different healing methods and the ability to stay safe at range.</p>

<p>The design got better not because I worked harder on it, but because I paused to think about what it <em>was</em> and what it <em>could be</em>.</p>

<p><img src="/assets/no-energy-week/defender.png" alt="defender" /></p>

<p>Same with the Voltage Defender. Its pull mechanic started simple—nudging creatures around the battlefield. But when I stopped to think about it, it shifted: what if it could move creatures <em>off islands entirely</em>? Suddenly it’s a broad control tool, shutting down buildings and repositioning the entire battlefield. That cascaded too—every ability needed rework. The EMP got bigger, affected every building including the player’s, made positioning critical.</p>

<p>This reflection loop, building something, stopping to think about it and then nudging the design. It keeps creating moments where the game gets better. It’s where iteration actually matters.</p>

<h2 id="the-real-lesson">The real lesson</h2>

<p>Turns out the structure matters more than the fuel. Low-energy weeks aren’t failures. They’re tests of process.</p>

<p>When you have nothing left, you find out what actually works. This week proved that good structure, spec-driven development, verification habits, and the discipline to reflect, can carry you through. The creatures got better not because I pushed harder, but because the process was solid enough to ship good work anyway.</p>

<p>I might not have had the energy. But the system did.</p>

<h2 id="next-week">Next week</h2>

<p>Fingers crossed, I will hopefully be finished the creature cards and start working on spells like Meteor and Lightning and getting some more buildings working correctly.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="godot" /><category term="vibecoding" /><category term="claudecode" /><category term="specdrivendevelopment" /><summary type="html"><![CDATA[The tale of how a good System can carry you through the dark times…]]></summary></entry><entry><title type="html">The Beautiful Boring: How I Refactored a Game Without Breaking it</title><link href="https://www.petervanonselen.com/2025/11/06/a-chill-refactor/" rel="alternate" type="text/html" title="The Beautiful Boring: How I Refactored a Game Without Breaking it" /><published>2025-11-06T08:00:00+00:00</published><updated>2025-11-06T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2025/11/06/a-chill-refactor</id><content type="html" xml:base="https://www.petervanonselen.com/2025/11/06/a-chill-refactor/"><![CDATA[<p><em>Can an AI Do a Boring Refactor? A Case Study in Systematic Code Cleanup</em></p>

<hr />

<p><img src="/assets/chill-refactor/banner.png" alt="banner" /></p>

<p>This week, my carefully-laid plans for rapid progress on <strong>Horizons Edge</strong>, a tactical wargame with card-driven combat on floating islands, collided headfirst with 2,255 lines of code and 94 functions living in a single file. Whoops. Just what I always wanted. I’d let a god class slowly brew and percolate while I focused on shipping features. Now it was time to pay the inevitable technical debt.</p>

<p>The wonderful file in question: <code class="language-plaintext highlighter-rouge">game_manager.gd</code>. It was managing twelve distinct functional areas: turn management, combat, creatures, cards, abilities, territory, players, and more. The result was tight coupling and maintenance friction of epic proportions.</p>

<h2 id="the-question-that-drove-me">The Question that drove me</h2>

<p>I’ve spent the last month learning how to work productively with AI on complex coding tasks. And I had a nagging question: <strong>Can an AI do a massive refactor productively? Can you have a boring refactor—a by-the-numbers, tick-the-boxes, super easy, chill refactor?</strong></p>

<p>This wasn’t just idle curiosity. When I last <a href="https://www.petervanonselen.com/2025/09/29/the-grand-refactor/">attempted a refactor of similar scope, it was an spectacular disaster</a>. I spent two evenings fighting with Claude about types, going in circles while the AI and I kept insisting the other was wrong. The game broke for days. It was a hair-pulling nightmare that never ended. It was painful, demoralizing, and left me deeply skeptical about whether AI could handle large-scale refactoring productively. That experience shaped everything about how I approached this week.</p>

<p>But this time felt different. <a href="https://www.petervanonselen.com/2025/10/30/exert-of-what-i-learnt/">I had a plan</a>.</p>

<p><img src="/assets/chill-refactor/plan.png" alt="a plan" /></p>

<h2 id="setting-the-constraints">Setting the Constraints</h2>

<p>Before diving in, I wanted to be intentional. I asked Claude to analyze the file and create a refactoring plan, but with strict requirements:</p>

<p><strong>Functional Requirements:</strong></p>
<ul>
  <li>Maintain existing behavior. No new code, no feature creep—same behavior, different structure.</li>
  <li>All key systems had to keep working: card play, creature combat, energy systems, turn management, terrain creation and destruction. Everything.</li>
</ul>

<p><strong>Technical Requirements:</strong></p>
<ul>
  <li>Files should be less than 400 lines each</li>
  <li>Absolutely no circular dependencies (this still haunts my nightmares)</li>
  <li>Follow good Godot practices using signal-based architecture and node-based composition</li>
</ul>

<p><strong>Methodology:</strong></p>
<ul>
  <li>Everything had to be incremental. No big bang refactors. Incremental changes mean incremental testing, which means catching bugs early.</li>
</ul>

<p>Claude produced a 1,091-line planning document comprising 11 phases, complete with a testing strategy, regression testing plan, and a high-level architecture with 8 new manager classes. It was exactly what I needed: a detailed roadmap to follow rather than a free-form creative challenge.</p>

<h2 id="the-method-that-worked">The Method That Worked</h2>

<p>Here’s the systematic approach I followed for every phase:</p>

<ol>
  <li><strong>Clean context</strong>: Open a new terminal window with a fresh Claude context (no conversation history creep)</li>
  <li><strong>Implement one phase</strong>: Ask Claude to implement just that single phase from the planning document</li>
  <li><strong>Verify no regressions</strong>: Get Claude to check to verify its work</li>
  <li><strong>Create a manual test plan</strong>: Have Claude outline a manual testing plan</li>
  <li><strong>Hand test and fix</strong>: Switch back to vibe coding; find bugs, fix them one at a time</li>
  <li><strong>Commit with clarity</strong>: Once working, commit to the branch with a descriptive message</li>
</ol>

<p>This rhythm was key. It prevented the cognitive overload of trying to refactor everything at once while still making steady progress.</p>

<h2 id="the-results">The Results</h2>

<p><img src="/assets/chill-refactor/gitcommit.png" alt="commit history" /></p>

<p>I started Friday evening and finished Tuesday evening. Seven hours total—an hour Friday night, four hours scattered across the weekend, another hour or two on Monday and Tuesday combined.</p>

<p>And here’s what shocked me: <strong>the game never broke</strong>. Not once. This was the easiest refactor of such a complicated system I’ve done in my career.</p>

<p>The difference was night and day compared to last month. That first refactor had been a hair-pulling nightmare. The game was down for days, I was fighting with the AI in circles, nothing felt under control. This time? The game was working the entire time. Every phase landed in manageable doses. I could test incrementally. I could fix bugs before they cascaded into system-wide failures.</p>

<p>It’s a quintessential example of risk mitigation in action. Small, verifiable steps beat big, catastrophic swings every time.</p>

<p>Unlike that previous disaster, this one was systematic. Methodical. Boring, even. It felt like just a matter of following good habits and executing. No dramatic debugging sessions. No circular reasoning about types. No late-night frustration.</p>

<p>There was something almost boring about how well it worked. And that was the point.</p>

<h2 id="what-this-taught-me">What This Taught Me</h2>

<p>The breakthrough wasn’t a better AI. It was a better process. By being intentional about constraints, breaking work into small phases, maintaining a testing regimen, and keeping context clean, I transformed a refactoring task from a high-risk, high-stress nightmare into a predictable, manageable project.</p>

<p>That first refactor failed because I was vibe-coding with the AI. It was reactive, unfocused, trying to solve the whole problem at once. This one succeeded because I treated it like a spec-driven project with a plan, clear objectives, and systematic execution.</p>

<p>The lesson: AI isn’t magic. It’s a tool. And like any tool, it works best when you know exactly what you’re trying to build and you approach it methodically.</p>

<h2 id="whats-next">What’s Next</h2>

<p>Now I can finally get back to what I actually want to be doing: making <strong>Horizons Edge</strong> a better game.</p>

<p><img src="/assets/chill-refactor/voltage-defender.png" alt="voltage defender" /></p>

<p>This coming week: three new creatures with three abilities each. <strong>Voltage Defender</strong> (exactly what it sounds like), <strong>Biomass Tank</strong> (a tanky presence), and <strong>Flux Chaos</strong> (honestly, even I don’t know what this one is yet. That’s kinda the fun of discovery). More abilities. More discipline. More checkboxes to tick.</p>

<p>Until next time, may your refactors be as boring, and your code as stable, as this one turned out to be.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="godot" /><category term="video-game" /><category term="claudecode" /><category term="vibecoding" /><summary type="html"><![CDATA[Can an AI Do a Boring Refactor? A Case Study in Systematic Code Cleanup]]></summary></entry><entry><title type="html">The Boring Path to Actually Shipping with AI</title><link href="https://www.petervanonselen.com/2025/10/31/boring-path-to-shipping/" rel="alternate" type="text/html" title="The Boring Path to Actually Shipping with AI" /><published>2025-10-31T08:00:00+00:00</published><updated>2025-10-31T08:00:00+00:00</updated><id>https://www.petervanonselen.com/2025/10/31/boring-path-to-shipping</id><content type="html" xml:base="https://www.petervanonselen.com/2025/10/31/boring-path-to-shipping/"><![CDATA[<p><em>Or: How I Learned to Stop Vibing and Love the Spec”</em></p>

<hr />

<p>OMG. This <a href="https://www.petervanonselen.com/2025/10/30/exert-of-what-i-learnt/">spec driven development</a> process is BORING!</p>

<p>Okay okay, for reals though, following this process of using a spec and a clear breakdown of tasks is tangibly yielding results and making remarkable progress forward in the game.</p>

<p><img src="/assets/boring/3-heroes.png" alt="3 heroes" /></p>

<p><strong>In the past week I have:</strong></p>
<ul>
  <li>Created 3 new creatures: one that moves fast, hits hard and stuns enemies; another that spawns minions and multiplies when it dies; and one that shoots a bolt that blows things up and destroys land all over the place</li>
  <li>Around 9 new abilities created and working</li>
  <li>Got the AI to hack some terrible models together so they would be unique enough to be playable</li>
  <li>Made the islands generate more interestingly</li>
  <li>Completed a horde of UI cleanups</li>
  <li>Handled some general refactorings and got a bunch of systems working</li>
  <li><strong>Total changes:</strong> 52 files modified, +3,718 lines, -474 lines across 34 commits all for about 10 hours effort.</li>
</ul>

<p>By basically all metrics… productive?</p>

<h2 id="so-where-did-the-boring-comment-come-from">So Where Did the “Boring” Comment Come From?</h2>

<p>It comes down to what following the spec driven development process has actually become. Now that I’m being militant about making AI follow a todo list, what I’ve functionally done is put on multiple hats:</p>

<p><strong>Product Manager hat:</strong> Created a complete game design doc. High-level, aspirational, covering combat systems, creatures, abilities, victory conditions — the whole vision thing.</p>

<p><strong>Delivery/Feature Lead hat:</strong> Took one section (the combat system) and broke it down into actual features. Not just “build combat” but “what does combat <em>need</em>? Movement? Attacks? Status effects? Death?” The unglamorous work of turning vibes into verbs.</p>

<p><strong>3 Amigos hat:</strong> Turned those features into a massive todo task list. Every checkbox a micro-commitment. “Add blink ability.” “Implement stun on hit.” “Make multiplying enemy spawn minions.” The kind of granular breakdown that makes you feel like you’re doing corporate sprint planning for your hobby project.</p>

<p><strong>Engineer hat:</strong> Actioning the tasks one at a time. No wandering off to make prettier models. No “oh but what if the islands had weather systems?” Just: checkbox, code, commit, next checkbox.</p>

<p><strong>QA hat:</strong> Testing behavior. Does the stun actually stun? Does the explosion destroy terrain properly? Do the spawned minions inherit the right stats? The tedious-but-essential validation loop.</p>

<p><strong>The realization:</strong> I’ve become …. an entire agile team.</p>

<p>And what that practically means is that I’ve made gamedev into <em>work</em>. My day job. God damn it.</p>

<p>Spent a lifetime developing habits on how to do engineering, and then you get a newfangled tool and you just… follow the process. Good job me. Yay! Right? …Right?!</p>

<h2 id="the-tangents-i-didnt-follow-and-why-that-hurts-a-little">The Tangents I Didn’t Follow (And Why That Hurts a Little)</h2>

<p>I’ll be honest: getting lost in tangents and running away with the vibes is a whole hell of a lot of fun.</p>

<p>In past weeks, I would have absolutely gone off on any of these:</p>

<p><strong>Visual polish:</strong> Using all the gorgeous tiles from Kenney’s asset packs to make everything look beautiful instead of just functional. Making each creature feel distinct and characterful instead of “placeholder cube with stats.”</p>

<p><strong>Procedural generation rabbit hole:</strong> Diving deeper into Wave Function Collapse algorithms to generate more dynamic, interesting terrain. Making islands that feel hand-crafted even though they’re algorithmic.</p>

<p><strong>Creature personality:</strong> Actually modeling unique designs for each bot. Giving them visual identity, animations, character beyond their mechanical function.</p>

<p><strong>Worldbuilding:</strong> Fleshing out the lore of the different factions. Their motivations, their aesthetics, their place in this weird sky-island world I’m building.</p>

<p>These are the <em>fun</em> parts. The parts where you lose track of time because you’re following curiosity instead of a checklist. The parts that make gamedev feel like <em>play</em> instead of <em>work</em>.</p>

<p>But here’s the thing: <strong>none of them get me closer to a playable game.</strong></p>

<p>They’re all polish on a foundation that doesn’t exist yet. They’re the dessert when I haven’t finished the vegetables. So the spec says: not now. Stay focused. Ship the MVP first.</p>

<p>It’s the right call. I know it’s the right call. Oh heavens please let this be the right call …</p>

<p>And I hate how boring the right call is.</p>

<h2 id="the-discipline-vs-fun-paradox">The Discipline vs. Fun Paradox</h2>

<p>Being strictly disciplined with myself about how to dev with this tool is <em>super productive</em>. Lots of forward momentum in an actual direction is really fantastic.</p>

<p>And also… boring.</p>

<p>There’s something deeply satisfying about seeing the commit graph fill up. About checking off todo items. About watching the line count grow in a structured, intentional way. It feels <em>professional</em>. It feels like I’m actually building something instead of just playing around.</p>

<p>But it’s missing that chaotic energy that made the early weeks of this project so intoxicating. The “what if I just try this wild thing?” moments. The tangents that turned into features I didn’t know I needed.</p>

<p>The spec process works. It’s just not romantic.</p>

<h2 id="where-im-headed">Where I’m Headed</h2>

<p>My plan is to stay disciplined until I hit what I’m calling an “exit point” — a milestone where the game is functioning <em>just enough</em> to validate the gameplay and experience. Right now, that means:</p>

<ul>
  <li><strong>2 unique decks</strong> that feel different to play</li>
  <li><strong>Basic strategy cards</strong> that offer meaningful choices</li>
  <li><strong>Fog of war</strong> (because exploration matters in a tactics game)</li>
  <li><strong>A simple victory condition</strong></li>
</ul>

<p>It won’t be <em>done</em>. But it will be <strong>playable</strong>. And <strong>testable</strong>. A real artifact I can put in front of someone and ask: “Is this fun?”</p>

<p>Following this process is giving me something I’ve never had before in side projects: <strong>predictable, consistent progress</strong>.</p>

<p>Not explosive bursts of inspiration followed by month-long abandonments. Not chasing vibes until I hit a wall and lose interest. Not 10,000-line notebooks that collapse under their own weight.</p>

<p>Actual, measurable forward motion toward a concrete goal.</p>

<p><img src="/assets/boring/chaos.png" alt="current" /></p>

<h2 id="the-bottom-line">The Bottom Line</h2>

<p>Is it boring? Yes.</p>

<p>Is it working? Also yes.</p>

<p>And maybe that’s the trade I need to make right now. There’s a time for tangents and vibes — I spent weeks in that mode and learned a ton. But there’s also a time to put your head down, follow the checklist, and <em>actually finish something</em>.</p>

<p>The irony isn’t lost on me: I spent four months learning how to use AI as a collaborator, only to discover that the real unlock was bringing back all the boring engineering discipline I use at my day job.</p>

<p>Turns out “vibe coding” still requires structure. Who knew?</p>

<p>Next week: More abilities. More discipline. More checkboxes. And hopefully, one step closer to knowing if this game is worth making at all.</p>

<hr />

<p><strong>P.S.</strong> If you’re following along with this devlog and thinking “wow, this sounds like he’s sucked all the joy out of his hobby” — yeah, a little bit. But also: I’m actually <em>building</em> something now instead of just dreaming about it. So maybe boring is the price of shipping.</p>

<p>We’ll see how I feel when I hit that exit point.</p>]]></content><author><name>Peter van Onselen</name><email>augury_upsurge.17@icloud.com</email><uri>https://www.petervanonselen.com</uri></author><category term="personal" /><category term="godot" /><category term="video-game" /><category term="claudecode" /><category term="vibecoding" /><summary type="html"><![CDATA[Or: How I Learned to Stop Vibing and Love the Spec”]]></summary></entry></feed>